slipway-cli 0.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.
@@ -0,0 +1,43 @@
1
+ import { c } from '../lib/colors.js'
2
+ import { api } from '../lib/api.js'
3
+ import { saveProjectConfig, isLoggedIn } from '../lib/config.js'
4
+ import { error, createSpinner } from '../lib/utils.js'
5
+
6
+ export default async function link(options, positionals) {
7
+ if (!isLoggedIn()) {
8
+ error('Not logged in. Run `slipway login` first.')
9
+ }
10
+
11
+ const projectSlug = positionals[0]
12
+
13
+ if (!projectSlug) {
14
+ error('Please provide a project slug. Usage: slipway link <project-slug>')
15
+ }
16
+
17
+ const spin = createSpinner(`Linking to project "${projectSlug}"...`).start()
18
+
19
+ try {
20
+ // Verify project exists and user has access
21
+ const { project } = await api.projects.get(projectSlug)
22
+
23
+ // Save project config
24
+ saveProjectConfig({
25
+ project: project.slug,
26
+ projectId: project.id
27
+ })
28
+
29
+ spin.succeed('Project linked')
30
+ console.log()
31
+ console.log(` ${c.dim('Project:')} ${project.name}`)
32
+ console.log(` ${c.dim('Slug:')} ${project.slug}`)
33
+ console.log()
34
+ console.log(` ${c.dim('Run')} ${c.highlight('slipway deploy')} ${c.dim('to deploy your app.')}`)
35
+ console.log()
36
+ } catch (err) {
37
+ spin.fail('Failed to link project')
38
+ if (err.statusCode === 404) {
39
+ error(`Project "${projectSlug}" not found. Check the project slug and try again.`)
40
+ }
41
+ error(err.message)
42
+ }
43
+ }
@@ -0,0 +1,205 @@
1
+ import { c } from '../lib/colors.js'
2
+ import { prompt, waitForEnter } from '../lib/prompt.js'
3
+ import { setCredentials, getCredentials } from '../lib/config.js'
4
+ import { error } from '../lib/utils.js'
5
+ import { spawn } from 'node:child_process'
6
+ import { platform } from 'node:os'
7
+
8
+ // Open URL in default browser (fire and forget)
9
+ function openBrowser(url) {
10
+ const plat = platform()
11
+
12
+ let cmd, args
13
+ if (plat === 'darwin') {
14
+ cmd = 'open'
15
+ args = [url]
16
+ } else if (plat === 'win32') {
17
+ cmd = 'cmd'
18
+ args = ['/c', 'start', '', url]
19
+ } else {
20
+ cmd = 'xdg-open'
21
+ args = [url]
22
+ }
23
+
24
+ try {
25
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true })
26
+ child.unref()
27
+ return true
28
+ } catch {
29
+ return false
30
+ }
31
+ }
32
+
33
+ export default async function login(options) {
34
+ console.log()
35
+ console.log(` ${c.bold(c.highlight('Slipway Login'))}`)
36
+ console.log()
37
+
38
+ // Get server URL from (in priority order):
39
+ // 1) --server flag, 2) SLIPWAY_SERVER env var, 3) saved credentials, 4) prompt user
40
+ let serverUrl = options.server
41
+ const savedCredentials = getCredentials()
42
+
43
+ if (!serverUrl && process.env.SLIPWAY_SERVER) {
44
+ // Use environment variable
45
+ serverUrl = process.env.SLIPWAY_SERVER
46
+ console.log(` ${c.dim('Server:')} ${serverUrl} ${c.dim('(from SLIPWAY_SERVER)')}`)
47
+ console.log()
48
+ } else if (!serverUrl && savedCredentials.server) {
49
+ // Use saved server URL
50
+ serverUrl = savedCredentials.server
51
+ console.log(` ${c.dim('Server:')} ${serverUrl}`)
52
+ console.log(` ${c.dim('(use --server to change)')}`)
53
+ console.log()
54
+ } else if (!serverUrl) {
55
+ // First time - prompt for server URL
56
+ console.log(` ${c.dim('Enter your Slipway server URL.')}`)
57
+ console.log(` ${c.dim('Example: https://slipway.yourcompany.com')}`)
58
+ console.log()
59
+ serverUrl = await prompt(' Server URL')
60
+
61
+ if (!serverUrl) {
62
+ error('Server URL is required')
63
+ }
64
+ }
65
+
66
+ // Normalize URL
67
+ serverUrl = serverUrl.replace(/\/$/, '')
68
+
69
+ try {
70
+ // Step 1: Request a login session from the server
71
+ const initResponse = await fetch(`${serverUrl}/api/v1/cli/auth/init`, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' }
74
+ })
75
+
76
+ if (!initResponse.ok) {
77
+ const body = await initResponse.json().catch(() => ({}))
78
+ error(body.message || 'Failed to initialize login session')
79
+ }
80
+
81
+ const { code, loginUrl } = await initResponse.json()
82
+
83
+ // Step 2: Show login info and wait for user to press ENTER
84
+ console.log(` ${c.dim('Your confirmation code:')} ${c.bold(code)}`)
85
+ console.log()
86
+ console.log(` ${c.dim('URL:')} ${c.highlight(loginUrl)}`)
87
+ console.log()
88
+
89
+ await waitForEnter(' Press ENTER to open in the browser...')
90
+
91
+ console.log()
92
+ console.log(` ${c.dim('Opening browser...')}`)
93
+
94
+ const opened = openBrowser(loginUrl)
95
+ if (!opened) {
96
+ console.log(` ${c.warn('Could not open browser automatically.')}`)
97
+ console.log(` ${c.dim('Please open the URL above manually.')}`)
98
+ }
99
+
100
+ console.log()
101
+ console.log(` ${c.dim('Waiting for you to authorize in the browser...')}`)
102
+ console.log()
103
+
104
+ // Step 3: Wait for authentication via polling (show dots for progress)
105
+ process.stdout.write(' Checking')
106
+
107
+ const result = await waitForAuthPolling(serverUrl, code, () => {
108
+ process.stdout.write('.')
109
+ })
110
+
111
+ console.log() // New line after dots
112
+
113
+ if (!result.authenticated) {
114
+ console.log(` ${c.error('✗')} ${result.error || 'Authentication failed'}`)
115
+ error('Please try again')
116
+ }
117
+
118
+ // Extract team from user data
119
+ const { team, ...user } = result.user || {}
120
+
121
+ // Step 4: Save credentials
122
+ setCredentials({
123
+ server: serverUrl,
124
+ token: result.token,
125
+ user: user.email ? user : null,
126
+ team: team || null
127
+ })
128
+
129
+ console.log(` ${c.success('✓')} Logged in successfully`)
130
+ console.log()
131
+
132
+ if (user.email) {
133
+ console.log(` ${c.dim('Logged in as:')} ${user.email}`)
134
+ if (team) {
135
+ console.log(` ${c.dim('Team:')} ${team.name}`)
136
+ }
137
+ console.log(` ${c.dim('Server:')} ${serverUrl}`)
138
+ }
139
+
140
+ console.log()
141
+ } catch (err) {
142
+ error(`Could not connect to server: ${err.message}`)
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Wait for auth via polling
148
+ */
149
+ async function waitForAuthPolling(serverUrl, code, onProgress) {
150
+ const maxAttempts = 120 // 2 minutes
151
+ let attempts = 0
152
+
153
+ while (attempts < maxAttempts) {
154
+ await sleep(1000)
155
+ attempts++
156
+
157
+ // Show progress
158
+ if (onProgress) onProgress()
159
+
160
+ try {
161
+ const response = await fetch(`${serverUrl}/api/v1/cli/auth/check`, {
162
+ method: 'POST',
163
+ headers: { 'Content-Type': 'application/json' },
164
+ body: JSON.stringify({ code })
165
+ })
166
+
167
+ if (response.ok) {
168
+ const result = await response.json()
169
+
170
+ if (result.status === 'authenticated') {
171
+ return {
172
+ authenticated: true,
173
+ token: result.token,
174
+ user: result.user
175
+ }
176
+ } else if (result.status === 'expired') {
177
+ return {
178
+ authenticated: false,
179
+ error: 'Login session expired'
180
+ }
181
+ }
182
+ // Still pending, continue polling
183
+ } else if (response.status === 404) {
184
+ // Session expired on server side, keep trying a few more times
185
+ if (attempts > 5) {
186
+ return {
187
+ authenticated: false,
188
+ error: 'Session expired on server'
189
+ }
190
+ }
191
+ }
192
+ } catch (err) {
193
+ // Network error, keep trying
194
+ }
195
+ }
196
+
197
+ return {
198
+ authenticated: false,
199
+ error: 'Authentication timed out'
200
+ }
201
+ }
202
+
203
+ function sleep(ms) {
204
+ return new Promise(resolve => setTimeout(resolve, ms))
205
+ }
@@ -0,0 +1,12 @@
1
+ import { clearCredentials, isLoggedIn } from '../lib/config.js'
2
+ import { success, info } from '../lib/utils.js'
3
+
4
+ export default async function logout() {
5
+ if (!isLoggedIn()) {
6
+ info('Not currently logged in')
7
+ return
8
+ }
9
+
10
+ clearCredentials()
11
+ success('Logged out successfully')
12
+ }
@@ -0,0 +1,88 @@
1
+ import { c } from '../lib/colors.js'
2
+ import { api } from '../lib/api.js'
3
+ import { isLoggedIn } from '../lib/config.js'
4
+ import { error, requireProject, createSpinner } from '../lib/utils.js'
5
+
6
+ export default async function logs(options) {
7
+ if (!isLoggedIn()) {
8
+ error('Not logged in. Run `slipway login` first.')
9
+ }
10
+
11
+ // If deployment ID is provided, show deployment logs
12
+ if (options.deployment) {
13
+ return showDeploymentLogs(options.deployment)
14
+ }
15
+
16
+ const project = requireProject()
17
+ const environment = options.env || 'production'
18
+
19
+ console.log()
20
+ console.log(` ${c.bold(c.highlight('Application Logs'))}`)
21
+ console.log(` ${c.dim(`${project.project} / ${environment}`)}`)
22
+ console.log()
23
+
24
+ try {
25
+ // Get environment to find container name
26
+ const { environment: env } = await api.environments.get(project.project, environment)
27
+
28
+ if (!env.app || env.app.length === 0) {
29
+ error('No app deployed in this environment. Run `slipway deploy` first.')
30
+ }
31
+
32
+ const app = env.app[0]
33
+ if (app.status !== 'running') {
34
+ error(`App is not running (status: ${app.status})`)
35
+ }
36
+
37
+ // For now, show instructions to view logs directly
38
+ // In a full implementation, we'd stream logs via WebSocket or polling
39
+ console.log(` ${c.dim('Container:')} ${app.containerName}`)
40
+ console.log()
41
+ console.log(` ${c.dim('To view logs, run:')}`)
42
+ console.log(` docker logs ${options.follow ? '-f ' : ''}--tail ${options.tail} ${app.containerName}`)
43
+ console.log()
44
+
45
+ // TODO: Implement log streaming via API
46
+ } catch (err) {
47
+ error(err.message)
48
+ }
49
+ }
50
+
51
+ async function showDeploymentLogs(deploymentId) {
52
+ console.log()
53
+ console.log(` ${c.bold(c.highlight('Deployment Logs'))}`)
54
+ console.log()
55
+
56
+ const spin = createSpinner('Fetching logs...').start()
57
+
58
+ try {
59
+ const result = await api.deployments.logs(deploymentId, 'all')
60
+
61
+ spin.stop()
62
+
63
+ console.log(` ${c.dim('Deployment:')} ${deploymentId}`)
64
+ console.log(` ${c.dim('Status:')} ${result.status}`)
65
+ console.log()
66
+
67
+ if (result.buildLogs) {
68
+ console.log(` ${c.bold(c.highlight('Build Logs:'))}`)
69
+ console.log(` ${c.dim('─'.repeat(50))}`)
70
+ console.log(result.buildLogs)
71
+ }
72
+
73
+ if (result.deployLogs) {
74
+ console.log(` ${c.bold(c.highlight('Deploy Logs:'))}`)
75
+ console.log(` ${c.dim('─'.repeat(50))}`)
76
+ console.log(result.deployLogs)
77
+ }
78
+
79
+ if (!result.buildLogs && !result.deployLogs) {
80
+ console.log(` ${c.dim('No logs available yet.')}`)
81
+ }
82
+
83
+ console.log()
84
+ } catch (err) {
85
+ spin.fail('Failed to fetch logs')
86
+ error(err.message)
87
+ }
88
+ }
@@ -0,0 +1,40 @@
1
+ import { c } from '../lib/colors.js'
2
+ import { api } from '../lib/api.js'
3
+ import { isLoggedIn } from '../lib/config.js'
4
+ import { error } from '../lib/utils.js'
5
+
6
+ export default async function projectUpdate(options, positionals) {
7
+ if (!isLoggedIn()) {
8
+ error('Not logged in. Run `slipway login` first.')
9
+ }
10
+
11
+ const slug = positionals[0]
12
+ if (!slug) {
13
+ error('Please provide a project slug. Usage: slipway project:update <slug> [options]')
14
+ }
15
+
16
+ const updates = {}
17
+ if (options.name !== undefined) updates.name = options.name
18
+ if (options.description !== undefined) updates.description = options.description
19
+ if (options.repo !== undefined) updates.repositoryUrl = options.repo
20
+
21
+ if (Object.keys(updates).length === 0) {
22
+ error('No updates provided. Use --name, --description, or --repo.')
23
+ }
24
+
25
+ try {
26
+ const { project } = await api.projects.update(slug, updates)
27
+
28
+ console.log()
29
+ console.log(`${c.success('✓')} Project updated`)
30
+ console.log()
31
+ console.log(` ${c.dim('Name:')} ${project.name}`)
32
+ console.log(` ${c.dim('Slug:')} ${project.slug}`)
33
+ if (project.description) {
34
+ console.log(` ${c.dim('Description:')} ${project.description}`)
35
+ }
36
+ console.log()
37
+ } catch (err) {
38
+ error(err.message)
39
+ }
40
+ }
@@ -0,0 +1,42 @@
1
+ import { c } from '../lib/colors.js'
2
+ import { api } from '../lib/api.js'
3
+ import { isLoggedIn } from '../lib/config.js'
4
+ import { error, table, formatDate } from '../lib/utils.js'
5
+
6
+ export default async function projects() {
7
+ if (!isLoggedIn()) {
8
+ error('Not logged in. Run `slipway login` first.')
9
+ }
10
+
11
+ console.log()
12
+ console.log(` ${c.bold(c.highlight('Projects'))}`)
13
+ console.log()
14
+
15
+ try {
16
+ const { projects } = await api.projects.list()
17
+
18
+ if (projects.length === 0) {
19
+ console.log(` ${c.dim('No projects yet. Run `slipway init` to create one.')}`)
20
+ console.log()
21
+ return
22
+ }
23
+
24
+ const rows = projects.map(p => [
25
+ p.name,
26
+ c.dim(p.slug),
27
+ p.environments ? `${p.environments.length}` : '0',
28
+ formatDate(p.updatedAt)
29
+ ])
30
+
31
+ table(
32
+ ['Name', 'Slug', 'Envs', 'Last updated'],
33
+ rows
34
+ )
35
+
36
+ console.log()
37
+ console.log(` ${c.dim(`${projects.length} project${projects.length !== 1 ? 's' : ''}`)}`)
38
+ console.log()
39
+ } catch (err) {
40
+ error(err.message)
41
+ }
42
+ }
@@ -0,0 +1,46 @@
1
+ import { c } from '../lib/colors.js'
2
+ import { api } from '../lib/api.js'
3
+ import { isLoggedIn } from '../lib/config.js'
4
+ import { error, requireProject } from '../lib/utils.js'
5
+
6
+ export default async function runCommand(options, positionals) {
7
+ if (!isLoggedIn()) {
8
+ error('Not logged in. Run `slipway login` first.')
9
+ }
10
+
11
+ if (positionals.length === 0) {
12
+ error('Please provide a command to run. Usage: slipway exec <command>')
13
+ }
14
+
15
+ const project = requireProject()
16
+ const environment = options.env || 'production'
17
+ const cmd = positionals.join(' ')
18
+
19
+ try {
20
+ // Get environment to find container name
21
+ const { environment: env } = await api.environments.get(project.project, environment)
22
+
23
+ if (!env.app || env.app.length === 0) {
24
+ error('No app deployed in this environment. Run `slipway deploy` first.')
25
+ }
26
+
27
+ const app = env.app[0]
28
+ if (app.status !== 'running') {
29
+ error(`App is not running (status: ${app.status})`)
30
+ }
31
+
32
+ console.log()
33
+ console.log(` ${c.dim('Container:')} ${app.containerName}`)
34
+ console.log(` ${c.dim('Command:')} ${cmd}`)
35
+ console.log()
36
+ console.log(` ${c.dim('To run this command, use:')}`)
37
+ console.log(` docker container run ${app.containerName} ${cmd}`)
38
+ console.log()
39
+
40
+ // TODO: In a production implementation, we would:
41
+ // 1. Run via API endpoint that uses docker on the server
42
+ // 2. Stream the output back to the CLI
43
+ } catch (err) {
44
+ error(err.message)
45
+ }
46
+ }
@@ -0,0 +1,68 @@
1
+ import { c } from '../lib/colors.js'
2
+ import { api } from '../lib/api.js'
3
+ import { isLoggedIn } from '../lib/config.js'
4
+ import { error, requireProject, table, statusColor } from '../lib/utils.js'
5
+
6
+ export default async function services(options) {
7
+ if (!isLoggedIn()) {
8
+ error('Not logged in. Run `slipway login` first.')
9
+ }
10
+
11
+ const project = requireProject()
12
+
13
+ console.log()
14
+ console.log(` ${c.bold(c.highlight('Services'))}`)
15
+ console.log()
16
+
17
+ try {
18
+ const { environments } = await api.environments.list(project.project)
19
+
20
+ // Filter by environment if specified
21
+ const targetEnvs = options.env
22
+ ? environments.filter(e => e.slug === options.env)
23
+ : environments
24
+
25
+ if (targetEnvs.length === 0) {
26
+ console.log(` ${c.dim('No environments found.')}`)
27
+ return
28
+ }
29
+
30
+ const allServices = []
31
+
32
+ for (const env of targetEnvs) {
33
+ const { services: envServices } = await api.services.list(project.project, env.slug)
34
+ if (envServices) {
35
+ envServices.forEach(s => {
36
+ allServices.push({
37
+ ...s,
38
+ environment: env.slug
39
+ })
40
+ })
41
+ }
42
+ }
43
+
44
+ if (allServices.length === 0) {
45
+ console.log(` ${c.dim('No services found.')}`)
46
+ console.log(` ${c.dim('Run')} ${c.highlight('slipway db:create <name>')} ${c.dim('to create a database.')}`)
47
+ console.log()
48
+ return
49
+ }
50
+
51
+ const rows = allServices.map(s => [
52
+ s.name,
53
+ s.type,
54
+ s.version || '-',
55
+ s.environment,
56
+ statusColor(s.status)
57
+ ])
58
+
59
+ table(
60
+ ['Name', 'Type', 'Version', 'Env', 'Status'],
61
+ rows
62
+ )
63
+
64
+ console.log()
65
+ } catch (err) {
66
+ error(err.message)
67
+ }
68
+ }