slipway-cli 0.0.0 → 0.0.1
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/README.md +4 -4
- package/package.json +1 -1
- package/src/commands/audit-log.js +54 -0
- package/src/commands/backup-create.js +46 -0
- package/src/commands/backup-list.js +65 -0
- package/src/commands/backup-restore.js +25 -0
- package/src/commands/deployments.js +2 -2
- package/src/commands/env-set.js +1 -1
- package/src/commands/env-unset.js +1 -1
- package/src/commands/link.js +1 -1
- package/src/commands/logs.js +10 -3
- package/src/commands/push.js +94 -0
- package/src/commands/run.js +10 -3
- package/src/commands/slide.js +12 -2
- package/src/commands/terminal.js +10 -3
- package/src/index.js +41 -5
- package/src/lib/api.js +34 -2
- package/src/lib/utils.js +9 -0
package/README.md
CHANGED
|
@@ -17,10 +17,10 @@ This means instant startup, no supply chain risk, and zero maintenance overhead.
|
|
|
17
17
|
## Commands
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
#
|
|
20
|
+
# Slide into production!
|
|
21
21
|
slipway slide # Primary command
|
|
22
|
-
slipway launch # Alias
|
|
23
22
|
slipway deploy # Alias
|
|
23
|
+
slipway launch # Alias
|
|
24
24
|
|
|
25
25
|
# Apps
|
|
26
26
|
slipway app:create myapp
|
|
@@ -53,9 +53,9 @@ slipway dev # Local dev mode
|
|
|
53
53
|
## Installation
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
npm install -g slipway
|
|
56
|
+
npm install -g slipway-cli
|
|
57
57
|
# or
|
|
58
|
-
npx slipway
|
|
58
|
+
npx slipway-cli
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
## Part of the Slipway Suite
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { c } from '../lib/colors.js'
|
|
2
|
+
import { api } from '../lib/api.js'
|
|
3
|
+
import { isLoggedIn } from '../lib/config.js'
|
|
4
|
+
import { error, createSpinner, table, formatDate } from '../lib/utils.js'
|
|
5
|
+
|
|
6
|
+
export default async function auditLog(options) {
|
|
7
|
+
if (!isLoggedIn()) {
|
|
8
|
+
error('Not logged in. Run `slipway login` first.')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const page = parseInt(options.page, 10) || 1
|
|
12
|
+
const limit = parseInt(options.limit, 10) || 20
|
|
13
|
+
|
|
14
|
+
const spin = createSpinner('Fetching audit logs...').start()
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const { logs, pagination } = await api.auditLogs.list(page, limit)
|
|
18
|
+
|
|
19
|
+
spin.stop()
|
|
20
|
+
|
|
21
|
+
console.log()
|
|
22
|
+
console.log(` ${c.bold(c.highlight('Audit Log'))} ${c.dim(`— page ${pagination.page} of ${pagination.totalPages}`)}`)
|
|
23
|
+
console.log()
|
|
24
|
+
|
|
25
|
+
if (!logs || logs.length === 0) {
|
|
26
|
+
console.log(` ${c.dim('No audit log entries found.')}`)
|
|
27
|
+
console.log()
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const rows = logs.map(log => [
|
|
32
|
+
formatDate(log.createdAt),
|
|
33
|
+
log.action,
|
|
34
|
+
log.resourceType,
|
|
35
|
+
log.userName,
|
|
36
|
+
log.ipAddress || 'N/A'
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
table(
|
|
40
|
+
['Date', 'Action', 'Resource', 'User', 'IP'],
|
|
41
|
+
rows
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if (pagination.page < pagination.totalPages) {
|
|
45
|
+
console.log()
|
|
46
|
+
console.log(` ${c.dim('Next page:')} ${c.highlight(`slipway audit-log --page ${pagination.page + 1}`)}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log()
|
|
50
|
+
} catch (err) {
|
|
51
|
+
spin.fail('Failed to fetch audit logs')
|
|
52
|
+
error(err.message)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -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, createSpinner } from '../lib/utils.js'
|
|
5
|
+
|
|
6
|
+
export default async function backupCreate(options, positionals) {
|
|
7
|
+
if (!isLoggedIn()) {
|
|
8
|
+
error('Not logged in. Run `slipway login` first.')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const name = positionals[0]
|
|
12
|
+
if (!name) {
|
|
13
|
+
error('Please provide a service name. Usage: slipway backup:create <service-name>')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const project = requireProject()
|
|
17
|
+
const environment = options.env || 'production'
|
|
18
|
+
|
|
19
|
+
const spin = createSpinner('Creating backup...').start()
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Resolve service name to ID
|
|
23
|
+
const { services } = await api.services.list(project.project, environment)
|
|
24
|
+
const service = services.find(s => s.name === name)
|
|
25
|
+
|
|
26
|
+
if (!service) {
|
|
27
|
+
spin.fail('Service not found')
|
|
28
|
+
error(`No service named "${name}" found in ${environment} environment.`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { backup } = await api.backups.create(service.id)
|
|
32
|
+
|
|
33
|
+
spin.succeed('Backup created')
|
|
34
|
+
|
|
35
|
+
console.log()
|
|
36
|
+
console.log(` ${c.dim('Backup ID:')} ${backup.id}`)
|
|
37
|
+
console.log(` ${c.dim('Status:')} ${backup.status}`)
|
|
38
|
+
console.log(` ${c.dim('Type:')} ${backup.type}`)
|
|
39
|
+
console.log()
|
|
40
|
+
console.log(` ${c.dim('Run')} ${c.highlight(`slipway backup:list ${name}`)} ${c.dim('to check progress.')}`)
|
|
41
|
+
console.log()
|
|
42
|
+
} catch (err) {
|
|
43
|
+
spin.fail('Failed to create backup')
|
|
44
|
+
error(err.message)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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, table, formatDate, formatBytes, formatDuration, statusColor } from '../lib/utils.js'
|
|
5
|
+
|
|
6
|
+
export default async function backupList(options, positionals) {
|
|
7
|
+
if (!isLoggedIn()) {
|
|
8
|
+
error('Not logged in. Run `slipway login` first.')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const name = positionals[0]
|
|
12
|
+
if (!name) {
|
|
13
|
+
error('Please provide a service name. Usage: slipway backup:list <service-name>')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const project = requireProject()
|
|
17
|
+
const environment = options.env || 'production'
|
|
18
|
+
|
|
19
|
+
const spin = createSpinner('Fetching backups...').start()
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Resolve service name to ID
|
|
23
|
+
const { services } = await api.services.list(project.project, environment)
|
|
24
|
+
const service = services.find(s => s.name === name)
|
|
25
|
+
|
|
26
|
+
if (!service) {
|
|
27
|
+
spin.fail('Service not found')
|
|
28
|
+
error(`No service named "${name}" found in ${environment} environment.`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { backups } = await api.backups.list(service.id)
|
|
32
|
+
|
|
33
|
+
spin.stop()
|
|
34
|
+
|
|
35
|
+
console.log()
|
|
36
|
+
console.log(` ${c.bold(c.highlight('Backups'))} ${c.dim(`— ${name} (${environment})`)}`)
|
|
37
|
+
console.log()
|
|
38
|
+
|
|
39
|
+
if (!backups || backups.length === 0) {
|
|
40
|
+
console.log(` ${c.dim('No backups found.')}`)
|
|
41
|
+
console.log(` ${c.dim('Run')} ${c.highlight(`slipway backup:create ${name}`)} ${c.dim('to create one.')}`)
|
|
42
|
+
console.log()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rows = backups.map(b => [
|
|
47
|
+
b.id,
|
|
48
|
+
statusColor(b.status),
|
|
49
|
+
b.type,
|
|
50
|
+
formatBytes(b.sizeBytes),
|
|
51
|
+
b.durationMs ? formatDuration(Math.round(b.durationMs / 1000)) : 'N/A',
|
|
52
|
+
formatDate(b.createdAt)
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
table(
|
|
56
|
+
['ID', 'Status', 'Type', 'Size', 'Duration', 'Date'],
|
|
57
|
+
rows
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
console.log()
|
|
61
|
+
} catch (err) {
|
|
62
|
+
spin.fail('Failed to fetch backups')
|
|
63
|
+
error(err.message)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { api } from '../lib/api.js'
|
|
2
|
+
import { isLoggedIn } from '../lib/config.js'
|
|
3
|
+
import { error, createSpinner } from '../lib/utils.js'
|
|
4
|
+
|
|
5
|
+
export default async function backupRestore(options, positionals) {
|
|
6
|
+
if (!isLoggedIn()) {
|
|
7
|
+
error('Not logged in. Run `slipway login` first.')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const backupId = positionals[0]
|
|
11
|
+
if (!backupId) {
|
|
12
|
+
error('Please provide a backup ID. Usage: slipway backup:restore <backup-id>')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const spin = createSpinner('Starting restore...').start()
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const result = await api.backups.restore(backupId)
|
|
19
|
+
|
|
20
|
+
spin.succeed(`Restore started for backup ${result.backupId}`)
|
|
21
|
+
} catch (err) {
|
|
22
|
+
spin.fail('Failed to start restore')
|
|
23
|
+
error(err.message)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -47,12 +47,12 @@ export default async function deployments(options) {
|
|
|
47
47
|
const limited = allDeployments.slice(0, parseInt(options.limit) || 10)
|
|
48
48
|
|
|
49
49
|
if (limited.length === 0) {
|
|
50
|
-
console.log(` ${c.dim('No deployments yet. Run `slipway
|
|
50
|
+
console.log(` ${c.dim('No deployments yet. Run `slipway slide` to create one.')}`)
|
|
51
51
|
return
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
const rows = limited.map(d => [
|
|
55
|
-
d.id
|
|
55
|
+
String(d.id),
|
|
56
56
|
statusColor(d.status),
|
|
57
57
|
d.environment,
|
|
58
58
|
d.gitBranch || '-',
|
package/src/commands/env-set.js
CHANGED
|
@@ -54,7 +54,7 @@ export default async function envSet(options, positionals) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
console.log()
|
|
57
|
-
console.log(` ${c.dim('Run')} ${c.highlight('slipway
|
|
57
|
+
console.log(` ${c.dim('Run')} ${c.highlight('slipway slide')} ${c.dim('to apply changes.')}`)
|
|
58
58
|
console.log()
|
|
59
59
|
} catch (err) {
|
|
60
60
|
spin.fail('Failed to set environment variables')
|
|
@@ -53,7 +53,7 @@ export default async function envUnset(options, positionals) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
console.log()
|
|
56
|
-
console.log(` ${c.dim('Run')} ${c.highlight('slipway
|
|
56
|
+
console.log(` ${c.dim('Run')} ${c.highlight('slipway slide')} ${c.dim('to apply changes.')}`)
|
|
57
57
|
console.log()
|
|
58
58
|
} catch (err) {
|
|
59
59
|
spin.fail('Failed to remove environment variables')
|
package/src/commands/link.js
CHANGED
|
@@ -31,7 +31,7 @@ export default async function link(options, positionals) {
|
|
|
31
31
|
console.log(` ${c.dim('Project:')} ${project.name}`)
|
|
32
32
|
console.log(` ${c.dim('Slug:')} ${project.slug}`)
|
|
33
33
|
console.log()
|
|
34
|
-
console.log(` ${c.dim('Run')} ${c.highlight('slipway
|
|
34
|
+
console.log(` ${c.dim('Run')} ${c.highlight('slipway slide')} ${c.dim('to deploy your app.')}`)
|
|
35
35
|
console.log()
|
|
36
36
|
} catch (err) {
|
|
37
37
|
spin.fail('Failed to link project')
|
package/src/commands/logs.js
CHANGED
|
@@ -25,11 +25,18 @@ export default async function logs(options) {
|
|
|
25
25
|
// Get environment to find container name
|
|
26
26
|
const { environment: env } = await api.environments.get(project.project, environment)
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const apps = env.app || []
|
|
29
|
+
if (apps.length === 0) {
|
|
30
|
+
error('No app deployed in this environment. Run `slipway slide` first.')
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
const app =
|
|
33
|
+
const app = options.app
|
|
34
|
+
? apps.find(a => a.slug === options.app)
|
|
35
|
+
: apps.find(a => a.isDefault) || apps[0]
|
|
36
|
+
|
|
37
|
+
if (!app) {
|
|
38
|
+
error(`App "${options.app}" not found. Available: ${apps.map(a => a.slug).join(', ')}`)
|
|
39
|
+
}
|
|
33
40
|
if (app.status !== 'running') {
|
|
34
41
|
error(`App is not running (status: ${app.status})`)
|
|
35
42
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { c } from '../lib/colors.js'
|
|
3
|
+
import { api } from '../lib/api.js'
|
|
4
|
+
import { isLoggedIn } from '../lib/config.js'
|
|
5
|
+
import { error, requireProject, createSpinner } from '../lib/utils.js'
|
|
6
|
+
|
|
7
|
+
export default async function push(options) {
|
|
8
|
+
if (!isLoggedIn()) {
|
|
9
|
+
error('Not logged in. Run `slipway login` first.')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const project = requireProject()
|
|
13
|
+
|
|
14
|
+
console.log()
|
|
15
|
+
console.log(` ${c.bold(c.highlight('Pushing'))} ${project.project}`)
|
|
16
|
+
console.log()
|
|
17
|
+
|
|
18
|
+
// 1. Package source code
|
|
19
|
+
const spin = createSpinner('Packaging source...').start()
|
|
20
|
+
|
|
21
|
+
let tarballBuffer
|
|
22
|
+
try {
|
|
23
|
+
tarballBuffer = createTarball()
|
|
24
|
+
const sizeMB = (tarballBuffer.length / 1024 / 1024).toFixed(1)
|
|
25
|
+
spin.succeed(`Source packaged (${sizeMB} MB)`)
|
|
26
|
+
} catch (err) {
|
|
27
|
+
spin.fail('Failed to package source')
|
|
28
|
+
error(err.message)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Upload source to server
|
|
32
|
+
const pushSpin = createSpinner('Pushing source...').start()
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await api.projects.push(project.project, tarballBuffer)
|
|
36
|
+
pushSpin.succeed('Source pushed')
|
|
37
|
+
} catch (err) {
|
|
38
|
+
pushSpin.fail('Failed to push source')
|
|
39
|
+
error(err.message)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log()
|
|
43
|
+
console.log(` ${c.success('✓')} Source code updated`)
|
|
44
|
+
console.log()
|
|
45
|
+
console.log(` ${c.dim('To deploy, run:')} ${c.highlight('slipway slide')}`)
|
|
46
|
+
console.log()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a tarball of the current directory.
|
|
51
|
+
* Uses `git archive` if in a git repo (respects .gitignore automatically),
|
|
52
|
+
* otherwise falls back to `tar` with sensible exclusions.
|
|
53
|
+
*/
|
|
54
|
+
function createTarball() {
|
|
55
|
+
const cwd = process.cwd()
|
|
56
|
+
|
|
57
|
+
if (isGitRepo(cwd)) {
|
|
58
|
+
return execFileSync('git', ['archive', '--format=tar.gz', 'HEAD'], {
|
|
59
|
+
cwd,
|
|
60
|
+
maxBuffer: 500 * 1024 * 1024
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback: tar with exclusions
|
|
65
|
+
const excludes = [
|
|
66
|
+
'node_modules',
|
|
67
|
+
'.git',
|
|
68
|
+
'.env',
|
|
69
|
+
'.DS_Store',
|
|
70
|
+
'*.log'
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
const args = ['czf', '-', ...excludes.flatMap(e => ['--exclude', e]), '.']
|
|
74
|
+
|
|
75
|
+
return execFileSync('tar', args, {
|
|
76
|
+
cwd,
|
|
77
|
+
maxBuffer: 500 * 1024 * 1024
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if a directory is inside a git repository.
|
|
83
|
+
*/
|
|
84
|
+
function isGitRepo(dir) {
|
|
85
|
+
try {
|
|
86
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
87
|
+
cwd: dir,
|
|
88
|
+
stdio: 'pipe'
|
|
89
|
+
})
|
|
90
|
+
return true
|
|
91
|
+
} catch {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/commands/run.js
CHANGED
|
@@ -20,11 +20,18 @@ export default async function runCommand(options, positionals) {
|
|
|
20
20
|
// Get environment to find container name
|
|
21
21
|
const { environment: env } = await api.environments.get(project.project, environment)
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const apps = env.app || []
|
|
24
|
+
if (apps.length === 0) {
|
|
25
|
+
error('No app deployed in this environment. Run `slipway slide` first.')
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const app =
|
|
28
|
+
const app = options.app
|
|
29
|
+
? apps.find(a => a.slug === options.app)
|
|
30
|
+
: apps.find(a => a.isDefault) || apps[0]
|
|
31
|
+
|
|
32
|
+
if (!app) {
|
|
33
|
+
error(`App "${options.app}" not found. Available: ${apps.map(a => a.slug).join(', ')}`)
|
|
34
|
+
}
|
|
28
35
|
if (app.status !== 'running') {
|
|
29
36
|
error(`App is not running (status: ${app.status})`)
|
|
30
37
|
}
|
package/src/commands/slide.js
CHANGED
|
@@ -49,13 +49,14 @@ export default async function slide(options) {
|
|
|
49
49
|
environment,
|
|
50
50
|
{
|
|
51
51
|
message: options.message,
|
|
52
|
+
appSlug: options.app,
|
|
52
53
|
...getGitInfo()
|
|
53
54
|
}
|
|
54
55
|
)
|
|
55
56
|
|
|
56
57
|
deploySpin.succeed('Deployment started')
|
|
57
58
|
console.log()
|
|
58
|
-
console.log(` ${c.dim('Deployment ID:')} ${deployment.id
|
|
59
|
+
console.log(` ${c.dim('Deployment ID:')} ${deployment.id}`)
|
|
59
60
|
console.log()
|
|
60
61
|
|
|
61
62
|
// 4. Watch deployment status via SSE (with polling fallback)
|
|
@@ -78,7 +79,7 @@ export default async function slide(options) {
|
|
|
78
79
|
} else if (result.status === 'failed') {
|
|
79
80
|
console.log(` ${c.error('✗')} Deployment failed`)
|
|
80
81
|
console.log()
|
|
81
|
-
console.log(` ${c.dim('Run')} ${c.highlight(`slipway logs -d ${deployment.id
|
|
82
|
+
console.log(` ${c.dim('Run')} ${c.highlight(`slipway logs -d ${deployment.id}`)} ${c.dim('to view logs.')}`)
|
|
82
83
|
console.log()
|
|
83
84
|
process.exit(1)
|
|
84
85
|
} else {
|
|
@@ -254,6 +255,15 @@ function watchDeploymentSSE(deploymentId) {
|
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
}
|
|
258
|
+
|
|
259
|
+
// Stream ended — resolve with last known status or fall back to polling
|
|
260
|
+
clearTimeout(timeout)
|
|
261
|
+
if (currentSpin) currentSpin.stop()
|
|
262
|
+
if (lastStatus && ['running', 'failed', 'cancelled'].includes(lastStatus)) {
|
|
263
|
+
resolve({ status: lastStatus })
|
|
264
|
+
} else {
|
|
265
|
+
reject(new Error('Stream ended before deployment completed'))
|
|
266
|
+
}
|
|
257
267
|
})
|
|
258
268
|
.catch((err) => {
|
|
259
269
|
clearTimeout(timeout)
|
package/src/commands/terminal.js
CHANGED
|
@@ -15,11 +15,18 @@ export default async function terminal(options) {
|
|
|
15
15
|
// Get environment to find container name
|
|
16
16
|
const { environment: env } = await api.environments.get(project.project, environment)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const apps = env.app || []
|
|
19
|
+
if (apps.length === 0) {
|
|
20
|
+
error('No app deployed in this environment. Run `slipway slide` first.')
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const app =
|
|
23
|
+
const app = options.app
|
|
24
|
+
? apps.find(a => a.slug === options.app)
|
|
25
|
+
: apps.find(a => a.isDefault) || apps[0]
|
|
26
|
+
|
|
27
|
+
if (!app) {
|
|
28
|
+
error(`App "${options.app}" not found. Available: ${apps.map(a => a.slug).join(', ')}`)
|
|
29
|
+
}
|
|
23
30
|
if (app.status !== 'running') {
|
|
24
31
|
error(`App is not running (status: ${app.status})`)
|
|
25
32
|
}
|
package/src/index.js
CHANGED
|
@@ -78,11 +78,16 @@ const commands = {
|
|
|
78
78
|
},
|
|
79
79
|
|
|
80
80
|
// Deployment commands
|
|
81
|
+
push: {
|
|
82
|
+
description: 'Push source code without deploying',
|
|
83
|
+
options: {}
|
|
84
|
+
},
|
|
81
85
|
slide: {
|
|
82
|
-
description: '
|
|
86
|
+
description: 'Push and deploy the current project',
|
|
83
87
|
aliases: ['deploy', 'launch'],
|
|
84
88
|
options: {
|
|
85
89
|
env: { type: 'string', short: 'e', default: 'production' },
|
|
90
|
+
app: { type: 'string', short: 'a' },
|
|
86
91
|
message: { type: 'string', short: 'm' }
|
|
87
92
|
}
|
|
88
93
|
},
|
|
@@ -97,6 +102,7 @@ const commands = {
|
|
|
97
102
|
description: 'View application logs',
|
|
98
103
|
options: {
|
|
99
104
|
env: { type: 'string', short: 'e', default: 'production' },
|
|
105
|
+
app: { type: 'string', short: 'a' },
|
|
100
106
|
follow: { type: 'boolean', short: 'f' },
|
|
101
107
|
tail: { type: 'string', short: 'n', default: '100' },
|
|
102
108
|
deployment: { type: 'string', short: 'd' }
|
|
@@ -151,18 +157,46 @@ const commands = {
|
|
|
151
157
|
}
|
|
152
158
|
},
|
|
153
159
|
|
|
160
|
+
// Backup commands
|
|
161
|
+
'backup:create': {
|
|
162
|
+
description: 'Create a manual database backup',
|
|
163
|
+
args: '<service-name>',
|
|
164
|
+
options: { env: { type: 'string', short: 'e', default: 'production' } }
|
|
165
|
+
},
|
|
166
|
+
'backup:list': {
|
|
167
|
+
description: 'List backups for a database service',
|
|
168
|
+
args: '<service-name>',
|
|
169
|
+
options: { env: { type: 'string', short: 'e', default: 'production' } }
|
|
170
|
+
},
|
|
171
|
+
'backup:restore': {
|
|
172
|
+
description: 'Restore a database backup',
|
|
173
|
+
args: '<backup-id>',
|
|
174
|
+
options: {}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// Admin commands
|
|
178
|
+
'audit-log': {
|
|
179
|
+
description: 'View audit log entries',
|
|
180
|
+
options: {
|
|
181
|
+
page: { type: 'string', short: 'p', default: '1' },
|
|
182
|
+
limit: { type: 'string', short: 'n', default: '20' }
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
154
186
|
// Container access
|
|
155
187
|
terminal: {
|
|
156
188
|
description: 'Open a terminal session in the running container',
|
|
157
189
|
options: {
|
|
158
|
-
env: { type: 'string', short: 'e', default: 'production' }
|
|
190
|
+
env: { type: 'string', short: 'e', default: 'production' },
|
|
191
|
+
app: { type: 'string', short: 'a' }
|
|
159
192
|
}
|
|
160
193
|
},
|
|
161
194
|
run: {
|
|
162
195
|
description: 'Run a command in the container',
|
|
163
196
|
args: '<command...>',
|
|
164
197
|
options: {
|
|
165
|
-
env: { type: 'string', short: 'e', default: 'production' }
|
|
198
|
+
env: { type: 'string', short: 'e', default: 'production' },
|
|
199
|
+
app: { type: 'string', short: 'a' }
|
|
166
200
|
}
|
|
167
201
|
}
|
|
168
202
|
}
|
|
@@ -182,11 +216,13 @@ function showHelp() {
|
|
|
182
216
|
'Authentication': ['login', 'logout', 'whoami'],
|
|
183
217
|
'Project': ['projects', 'project:update', 'init', 'link'],
|
|
184
218
|
'Environments': ['environments', 'environment:create', 'environment:update'],
|
|
185
|
-
'Deployment': ['slide', 'deployments', 'logs'],
|
|
219
|
+
'Deployment': ['push', 'slide', 'deployments', 'logs'],
|
|
186
220
|
'Database': ['db:create', 'db:url'],
|
|
187
221
|
'Services': ['services'],
|
|
222
|
+
'Backups': ['backup:create', 'backup:list', 'backup:restore'],
|
|
188
223
|
'Env Variables': ['env', 'env:set', 'env:unset'],
|
|
189
|
-
'Container': ['terminal', 'run']
|
|
224
|
+
'Container': ['terminal', 'run'],
|
|
225
|
+
'Admin': ['audit-log']
|
|
190
226
|
}
|
|
191
227
|
|
|
192
228
|
for (const [groupName, cmds] of Object.entries(groups)) {
|
package/src/lib/api.js
CHANGED
|
@@ -33,7 +33,17 @@ async function apiRequest(method, path, options = {}) {
|
|
|
33
33
|
|
|
34
34
|
try {
|
|
35
35
|
const response = await fetch(url, fetchOptions)
|
|
36
|
-
const
|
|
36
|
+
const text = await response.text()
|
|
37
|
+
|
|
38
|
+
let body
|
|
39
|
+
try {
|
|
40
|
+
body = JSON.parse(text)
|
|
41
|
+
} catch {
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new APIError(text || `Request failed with status ${response.status}`, response.status)
|
|
44
|
+
}
|
|
45
|
+
throw new APIError(`Unexpected response from server: ${text}`, response.status)
|
|
46
|
+
}
|
|
37
47
|
|
|
38
48
|
if (!response.ok) {
|
|
39
49
|
const message = body.message || body.error || `Request failed with status ${response.status}`
|
|
@@ -73,7 +83,17 @@ async function apiUpload(path, fieldName, buffer, filename) {
|
|
|
73
83
|
body: formData
|
|
74
84
|
})
|
|
75
85
|
|
|
76
|
-
const
|
|
86
|
+
const text = await response.text()
|
|
87
|
+
|
|
88
|
+
let body
|
|
89
|
+
try {
|
|
90
|
+
body = JSON.parse(text)
|
|
91
|
+
} catch {
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new APIError(text || `Upload failed with status ${response.status}`, response.status)
|
|
94
|
+
}
|
|
95
|
+
throw new APIError(`Unexpected response from server: ${text}`, response.status)
|
|
96
|
+
}
|
|
77
97
|
|
|
78
98
|
if (!response.ok) {
|
|
79
99
|
const message = body.message || body.error || `Upload failed with status ${response.status}`
|
|
@@ -134,3 +154,15 @@ api.services = {
|
|
|
134
154
|
get: (id) => api.get(`/services/${id}`),
|
|
135
155
|
delete: (id) => api.delete(`/services/${id}`)
|
|
136
156
|
}
|
|
157
|
+
|
|
158
|
+
// Backup endpoints
|
|
159
|
+
api.backups = {
|
|
160
|
+
create: (serviceId) => api.post(`/services/${serviceId}/backups`),
|
|
161
|
+
list: (serviceId) => api.get(`/services/${serviceId}/backups`),
|
|
162
|
+
restore: (backupId) => api.post(`/backups/${backupId}/restore`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Audit log endpoints
|
|
166
|
+
api.auditLogs = {
|
|
167
|
+
list: (page = 1, limit = 20) => api.get(`/audit-logs?page=${page}&limit=${limit}`)
|
|
168
|
+
}
|
package/src/lib/utils.js
CHANGED
|
@@ -32,6 +32,15 @@ export function formatDate(timestamp) {
|
|
|
32
32
|
return date.toLocaleString()
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function formatBytes(bytes) {
|
|
36
|
+
if (!bytes) return 'N/A'
|
|
37
|
+
const units = ['B', 'KB', 'MB', 'GB']
|
|
38
|
+
let i = 0
|
|
39
|
+
let size = bytes
|
|
40
|
+
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++ }
|
|
41
|
+
return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
export function formatDuration(seconds) {
|
|
36
45
|
if (!seconds) return 'N/A'
|
|
37
46
|
if (seconds < 60) return `${seconds}s`
|