sloss-cli 1.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.
- package/README.md +60 -0
- package/bin/sloss.js +103 -0
- package/package.json +27 -0
- package/src/client.js +91 -0
- package/src/commands/delete.js +17 -0
- package/src/commands/info.js +17 -0
- package/src/commands/list.js +15 -0
- package/src/commands/login.js +143 -0
- package/src/commands/upload.js +33 -0
- package/src/config.js +68 -0
- package/src/format.js +101 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# sloss-cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for [Sloss](https://github.com/aualdrich/sloss) — a self-hosted IPA/APK distribution server inspired by Diawi.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g sloss-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Authenticate (saves API key to ~/.config/sloss/credentials.json)
|
|
15
|
+
sloss login
|
|
16
|
+
|
|
17
|
+
# List recent builds
|
|
18
|
+
sloss list
|
|
19
|
+
|
|
20
|
+
# Upload an IPA
|
|
21
|
+
sloss upload path/to/App.ipa --platform ios --profile preview
|
|
22
|
+
|
|
23
|
+
# Upload an APK
|
|
24
|
+
sloss upload path/to/app.apk --platform android --profile development
|
|
25
|
+
|
|
26
|
+
# Get build details
|
|
27
|
+
sloss info <build-id>
|
|
28
|
+
|
|
29
|
+
# Delete a build
|
|
30
|
+
sloss delete <build-id>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Authentication
|
|
34
|
+
|
|
35
|
+
API key resolution order (highest → lowest priority):
|
|
36
|
+
1. `--api-key` CLI flag
|
|
37
|
+
2. `SLOSS_API_KEY` environment variable
|
|
38
|
+
3. `~/.config/sloss/credentials.json` (saved by `sloss login`)
|
|
39
|
+
|
|
40
|
+
## Server URL
|
|
41
|
+
|
|
42
|
+
URL resolution order:
|
|
43
|
+
1. `--url` CLI flag
|
|
44
|
+
2. `SLOSS_URL` environment variable
|
|
45
|
+
3. `~/.config/sloss/credentials.json`
|
|
46
|
+
4. Default: `https://sloss.ngrok.app`
|
|
47
|
+
|
|
48
|
+
## Global Options
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
--api-key <key> Override API key
|
|
52
|
+
--url <url> Override server URL
|
|
53
|
+
--json Output as JSON
|
|
54
|
+
--version Show version
|
|
55
|
+
--help Show help
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/bin/sloss.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sloss CLI - Command-line interface for Sloss IPA/APK build server
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { resolveConfig, resolveUrl } from '../src/config.js';
|
|
9
|
+
import { loginCommand } from '../src/commands/login.js';
|
|
10
|
+
import { listCommand } from '../src/commands/list.js';
|
|
11
|
+
import { uploadCommand } from '../src/commands/upload.js';
|
|
12
|
+
import { infoCommand } from '../src/commands/info.js';
|
|
13
|
+
import { deleteCommand } from '../src/commands/delete.js';
|
|
14
|
+
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name('sloss')
|
|
19
|
+
.description('CLI for Sloss IPA/APK build server')
|
|
20
|
+
.version('1.0.0')
|
|
21
|
+
.option('--api-key <key>', 'API key (overrides SLOSS_API_KEY env var)')
|
|
22
|
+
.option('--url <url>', 'Server URL (overrides SLOSS_URL env var)')
|
|
23
|
+
.option('--json', 'Output as JSON instead of human-readable format');
|
|
24
|
+
|
|
25
|
+
// Login command
|
|
26
|
+
program
|
|
27
|
+
.command('login')
|
|
28
|
+
.description('Authenticate and save your API key to sloss.json')
|
|
29
|
+
.option('--email <email>', 'Email address')
|
|
30
|
+
.option('--password <password>', 'Password (prefer interactive prompt)')
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
try {
|
|
33
|
+
const baseUrl = resolveUrl(program.opts());
|
|
34
|
+
await loginCommand(options, baseUrl);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`Error: ${error.message}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// List command
|
|
42
|
+
program
|
|
43
|
+
.command('list')
|
|
44
|
+
.description('List recent builds')
|
|
45
|
+
.option('--limit <number>', 'Maximum number of builds to list', '10')
|
|
46
|
+
.action(async (options) => {
|
|
47
|
+
try {
|
|
48
|
+
const config = resolveConfig(program.opts());
|
|
49
|
+
await listCommand(options, config);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`Error: ${error.message}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Upload command
|
|
57
|
+
program
|
|
58
|
+
.command('upload <file>')
|
|
59
|
+
.description('Upload an IPA or APK file')
|
|
60
|
+
.requiredOption('--platform <platform>', 'Platform (ios or android)')
|
|
61
|
+
.option('--app-name <name>', 'App name')
|
|
62
|
+
.option('--version <version>', 'Version number')
|
|
63
|
+
.option('--build-number <number>', 'Build number')
|
|
64
|
+
.option('--profile <profile>', 'Build profile (development, preview, production)')
|
|
65
|
+
.action(async (file, options) => {
|
|
66
|
+
try {
|
|
67
|
+
const config = resolveConfig(program.opts());
|
|
68
|
+
await uploadCommand(file, options, config);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(`Error: ${error.message}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Info command
|
|
76
|
+
program
|
|
77
|
+
.command('info <build-id>')
|
|
78
|
+
.description('Get details about a specific build')
|
|
79
|
+
.action(async (buildId) => {
|
|
80
|
+
try {
|
|
81
|
+
const config = resolveConfig(program.opts());
|
|
82
|
+
await infoCommand(buildId, config);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(`Error: ${error.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Delete command
|
|
90
|
+
program
|
|
91
|
+
.command('delete <build-id>')
|
|
92
|
+
.description('Delete a build')
|
|
93
|
+
.action(async (buildId) => {
|
|
94
|
+
try {
|
|
95
|
+
const config = resolveConfig(program.opts());
|
|
96
|
+
await deleteCommand(buildId, config);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`Error: ${error.message}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sloss-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for Sloss — a self-hosted IPA/APK distribution server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sloss": "./bin/sloss.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": ["sloss", "ios", "android", "ipa", "apk", "build", "distribution", "testflight", "diawi"],
|
|
15
|
+
"author": "Front Porch Software",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/aualdrich/sloss-cli.git"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.0.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"commander": "^12.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for Sloss API
|
|
3
|
+
* Uses native fetch (Node 18+)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class SlossClient {
|
|
7
|
+
constructor(baseUrl, apiKey) {
|
|
8
|
+
this.baseUrl = baseUrl;
|
|
9
|
+
this.apiKey = apiKey;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async request(method, path, body = null) {
|
|
13
|
+
const url = `${this.baseUrl}${path}`;
|
|
14
|
+
const headers = {
|
|
15
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const options = { method, headers };
|
|
19
|
+
|
|
20
|
+
if (body && !(body instanceof FormData)) {
|
|
21
|
+
headers['Content-Type'] = 'application/json';
|
|
22
|
+
options.body = JSON.stringify(body);
|
|
23
|
+
} else if (body instanceof FormData) {
|
|
24
|
+
options.body = body;
|
|
25
|
+
// Don't set Content-Type for FormData - fetch will set it with boundary
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const response = await fetch(url, options);
|
|
29
|
+
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
let errorMsg = `HTTP ${response.status} ${response.statusText}`;
|
|
32
|
+
try {
|
|
33
|
+
const errorBody = await response.json();
|
|
34
|
+
if (errorBody.error) {
|
|
35
|
+
errorMsg = errorBody.error;
|
|
36
|
+
if (errorBody.detail) errorMsg += `: ${errorBody.detail}`;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// If JSON parsing fails, use the generic message
|
|
40
|
+
}
|
|
41
|
+
throw new Error(errorMsg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Handle 204 No Content
|
|
45
|
+
if (response.status === 204) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return await response.json();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async listUploads(limit = 10) {
|
|
53
|
+
return await this.request('GET', `/api/uploads?limit=${limit}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getUpload(id) {
|
|
57
|
+
return await this.request('GET', `/api/uploads/${id}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async deleteUpload(id) {
|
|
61
|
+
return await this.request('DELETE', `/api/uploads/${id}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async uploadFile(filePath, platform, metadata = {}) {
|
|
65
|
+
const fs = await import('fs/promises');
|
|
66
|
+
const path = await import('path');
|
|
67
|
+
|
|
68
|
+
// Read file as buffer
|
|
69
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
70
|
+
const fileName = path.basename(filePath);
|
|
71
|
+
|
|
72
|
+
// Create FormData
|
|
73
|
+
const formData = new FormData();
|
|
74
|
+
|
|
75
|
+
// Determine field name based on platform
|
|
76
|
+
const fieldName = platform === 'android' ? 'apk' : 'ipa';
|
|
77
|
+
const blob = new Blob([fileBuffer]);
|
|
78
|
+
formData.append(fieldName, blob, fileName);
|
|
79
|
+
|
|
80
|
+
// Add platform
|
|
81
|
+
formData.append('platform', platform);
|
|
82
|
+
|
|
83
|
+
// Add optional metadata fields
|
|
84
|
+
if (metadata.appName) formData.append('app_name', metadata.appName);
|
|
85
|
+
if (metadata.version) formData.append('version', metadata.version);
|
|
86
|
+
if (metadata.buildNumber) formData.append('build_number', metadata.buildNumber);
|
|
87
|
+
if (metadata.profile) formData.append('profile', metadata.profile);
|
|
88
|
+
|
|
89
|
+
return await this.request('POST', '/upload', formData);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delete command - Delete a build
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SlossClient } from '../client.js';
|
|
6
|
+
import { formatDelete } from '../format.js';
|
|
7
|
+
|
|
8
|
+
export async function deleteCommand(buildId, config) {
|
|
9
|
+
if (!buildId) {
|
|
10
|
+
throw new Error('Build ID is required');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const client = new SlossClient(config.baseUrl, config.apiKey);
|
|
14
|
+
await client.deleteUpload(buildId);
|
|
15
|
+
|
|
16
|
+
console.log(formatDelete(config.jsonMode));
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Info command - Get details about a specific build
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SlossClient } from '../client.js';
|
|
6
|
+
import { formatInfo } from '../format.js';
|
|
7
|
+
|
|
8
|
+
export async function infoCommand(buildId, config) {
|
|
9
|
+
if (!buildId) {
|
|
10
|
+
throw new Error('Build ID is required');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const client = new SlossClient(config.baseUrl, config.apiKey);
|
|
14
|
+
const upload = await client.getUpload(buildId);
|
|
15
|
+
|
|
16
|
+
console.log(formatInfo(upload, config.jsonMode));
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List command - List recent builds
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SlossClient } from '../client.js';
|
|
6
|
+
import { formatList } from '../format.js';
|
|
7
|
+
|
|
8
|
+
export async function listCommand(options, config) {
|
|
9
|
+
const limit = parseInt(options.limit, 10) || 10;
|
|
10
|
+
|
|
11
|
+
const client = new SlossClient(config.baseUrl, config.apiKey);
|
|
12
|
+
const uploads = await client.listUploads(limit);
|
|
13
|
+
|
|
14
|
+
console.log(formatList(uploads, config.jsonMode));
|
|
15
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login command - Authenticate with Sloss and store API key locally
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Prompt for email + password (password hidden)
|
|
6
|
+
* 2. POST /auth/login → Cognito JWT
|
|
7
|
+
* 3. GET /api-keys (with JWT) → user's API key UUID
|
|
8
|
+
* 4. Write API key to sloss.json in cwd
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createInterface } from 'readline';
|
|
12
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { homedir } from 'os';
|
|
15
|
+
|
|
16
|
+
const CREDENTIALS_PATH = join(homedir(), '.config', 'sloss', 'credentials.json');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Prompt for a value, optionally hiding input (for passwords)
|
|
20
|
+
*/
|
|
21
|
+
function prompt(question, hidden = false) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const rl = createInterface({
|
|
24
|
+
input: process.stdin,
|
|
25
|
+
output: process.stdout,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (hidden) {
|
|
29
|
+
// Suppress echoed characters for password input
|
|
30
|
+
process.stdout.write(question);
|
|
31
|
+
|
|
32
|
+
let value = '';
|
|
33
|
+
|
|
34
|
+
const onData = (char) => {
|
|
35
|
+
char = char.toString();
|
|
36
|
+
if (char === '\n' || char === '\r' || char === '\u0004') {
|
|
37
|
+
// Enter pressed
|
|
38
|
+
process.stdin.setRawMode(false);
|
|
39
|
+
process.stdin.pause();
|
|
40
|
+
process.stdin.removeListener('data', onData);
|
|
41
|
+
process.stdout.write('\n');
|
|
42
|
+
rl.close();
|
|
43
|
+
resolve(value);
|
|
44
|
+
} else if (char === '\u0003') {
|
|
45
|
+
// Ctrl+C
|
|
46
|
+
process.stdout.write('\n');
|
|
47
|
+
process.exit(0);
|
|
48
|
+
} else if (char === '\u007f' || char === '\b') {
|
|
49
|
+
// Backspace
|
|
50
|
+
if (value.length > 0) {
|
|
51
|
+
value = value.slice(0, -1);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
value += char;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
process.stdin.setRawMode(true);
|
|
59
|
+
process.stdin.resume();
|
|
60
|
+
process.stdin.setEncoding('utf8');
|
|
61
|
+
process.stdin.on('data', onData);
|
|
62
|
+
} else {
|
|
63
|
+
rl.question(question, (answer) => {
|
|
64
|
+
rl.close();
|
|
65
|
+
resolve(answer);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function loginCommand(options, baseUrl) {
|
|
72
|
+
const email = options.email || await prompt('Email: ');
|
|
73
|
+
const password = options.password || await prompt('Password: ', true);
|
|
74
|
+
|
|
75
|
+
// Step 1: Authenticate → get Cognito JWT
|
|
76
|
+
process.stdout.write('Authenticating...');
|
|
77
|
+
|
|
78
|
+
let jwt;
|
|
79
|
+
try {
|
|
80
|
+
const loginRes = await fetch(`${baseUrl}/auth/login`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ email, password }),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const loginBody = await loginRes.json();
|
|
87
|
+
|
|
88
|
+
if (!loginRes.ok || !loginBody.success) {
|
|
89
|
+
process.stdout.write(' ✗\n');
|
|
90
|
+
throw new Error(loginBody.error || `Login failed (HTTP ${loginRes.status})`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
jwt = loginBody.token;
|
|
94
|
+
process.stdout.write(' ✓\n');
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err.message.includes('fetch')) {
|
|
97
|
+
throw new Error(`Cannot reach Sloss server at ${baseUrl}`);
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Step 2: Fetch the user's API key using the JWT
|
|
103
|
+
process.stdout.write('Fetching API key...');
|
|
104
|
+
|
|
105
|
+
let apiKey;
|
|
106
|
+
const keysRes = await fetch(`${baseUrl}/api-keys`, {
|
|
107
|
+
headers: { Authorization: `Bearer ${jwt}` },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const keysBody = await keysRes.json();
|
|
111
|
+
|
|
112
|
+
if (!keysRes.ok) {
|
|
113
|
+
process.stdout.write(' ✗\n');
|
|
114
|
+
throw new Error(keysBody.error || `Failed to fetch API key (HTTP ${keysRes.status})`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
apiKey = keysBody.apiKey?.id;
|
|
118
|
+
|
|
119
|
+
// If user has no API key yet, generate one
|
|
120
|
+
if (!apiKey) {
|
|
121
|
+
const genRes = await fetch(`${baseUrl}/api-keys/generate`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { Authorization: `Bearer ${jwt}` },
|
|
124
|
+
});
|
|
125
|
+
const genBody = await genRes.json();
|
|
126
|
+
if (!genRes.ok || !genBody.success) {
|
|
127
|
+
process.stdout.write(' ✗\n');
|
|
128
|
+
throw new Error(genBody.error || 'Failed to generate API key');
|
|
129
|
+
}
|
|
130
|
+
apiKey = genBody.apiKey.id;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
process.stdout.write(' ✓\n');
|
|
134
|
+
|
|
135
|
+
// Step 3: Write credentials to ~/.config/sloss/credentials.json
|
|
136
|
+
const config = { apiKey, url: baseUrl, email };
|
|
137
|
+
mkdirSync(join(homedir(), '.config', 'sloss'), { recursive: true });
|
|
138
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
139
|
+
|
|
140
|
+
console.log(`\n✅ Logged in as ${email}`);
|
|
141
|
+
console.log(` API key saved to ${CREDENTIALS_PATH}`);
|
|
142
|
+
console.log(`\n Add to your shell (optional):\n export SLOSS_API_KEY="${apiKey}"`);
|
|
143
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload command - Upload an IPA or APK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SlossClient } from '../client.js';
|
|
6
|
+
import { formatUpload } from '../format.js';
|
|
7
|
+
|
|
8
|
+
export async function uploadCommand(filePath, options, config) {
|
|
9
|
+
if (!filePath) {
|
|
10
|
+
throw new Error('File path is required');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!options.platform) {
|
|
14
|
+
throw new Error('Platform is required (--platform ios or --platform android)');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const platform = options.platform.toLowerCase();
|
|
18
|
+
if (!['ios', 'android'].includes(platform)) {
|
|
19
|
+
throw new Error('Platform must be either "ios" or "android"');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const metadata = {
|
|
23
|
+
appName: options.appName,
|
|
24
|
+
version: options.version,
|
|
25
|
+
buildNumber: options.buildNumber,
|
|
26
|
+
profile: options.profile,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const client = new SlossClient(config.baseUrl, config.apiKey);
|
|
30
|
+
const result = await client.uploadFile(filePath, platform, metadata);
|
|
31
|
+
|
|
32
|
+
console.log(formatUpload(result, config.jsonMode));
|
|
33
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration resolver for Sloss CLI
|
|
3
|
+
*
|
|
4
|
+
* API key resolution order (highest → lowest priority):
|
|
5
|
+
* 1. --api-key CLI flag
|
|
6
|
+
* 2. SLOSS_API_KEY environment variable
|
|
7
|
+
* 3. apiKey field in ~/.config/sloss/credentials.json
|
|
8
|
+
*
|
|
9
|
+
* URL resolution order:
|
|
10
|
+
* 1. --url CLI flag
|
|
11
|
+
* 2. SLOSS_URL environment variable
|
|
12
|
+
* 3. url field in ~/.config/sloss/credentials.json
|
|
13
|
+
* 4. Default production URL
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { homedir } from 'os';
|
|
19
|
+
|
|
20
|
+
const CREDENTIALS_PATH = join(homedir(), '.config', 'sloss', 'credentials.json');
|
|
21
|
+
const DEFAULT_URL = 'https://sloss.ngrok.app';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load and parse ~/.config/sloss/credentials.json, returning {} if not found / invalid.
|
|
25
|
+
*/
|
|
26
|
+
function loadCredentials() {
|
|
27
|
+
try {
|
|
28
|
+
const raw = readFileSync(CREDENTIALS_PATH, 'utf8');
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveConfig(options = {}) {
|
|
36
|
+
const file = loadCredentials();
|
|
37
|
+
|
|
38
|
+
// API Key: flag > env > sloss.json > error
|
|
39
|
+
const apiKey = options.apiKey || process.env.SLOSS_API_KEY || file.apiKey;
|
|
40
|
+
if (!apiKey) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'No API key found.\n' +
|
|
43
|
+
' Run `sloss login` to authenticate, or\n' +
|
|
44
|
+
' set SLOSS_API_KEY env var, or\n' +
|
|
45
|
+
' pass --api-key <key>'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// URL: flag > env > sloss.json > default
|
|
50
|
+
const rawUrl = options.url || process.env.SLOSS_URL || file.url || DEFAULT_URL;
|
|
51
|
+
const baseUrl = rawUrl.replace(/\/$/, '');
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
apiKey,
|
|
55
|
+
baseUrl,
|
|
56
|
+
jsonMode: options.json || false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve just the base URL (no API key required).
|
|
62
|
+
* Used by commands like `login` that don't have a key yet.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveUrl(options = {}) {
|
|
65
|
+
const file = loadCredentials();
|
|
66
|
+
const rawUrl = options.url || process.env.SLOSS_URL || file.url || DEFAULT_URL;
|
|
67
|
+
return rawUrl.replace(/\/$/, '');
|
|
68
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatters for Sloss CLI
|
|
3
|
+
* Handles both human-readable table output and JSON mode
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function formatList(uploads, jsonMode = false) {
|
|
7
|
+
if (jsonMode) {
|
|
8
|
+
return JSON.stringify(uploads, null, 2);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (uploads.length === 0) {
|
|
12
|
+
return 'No builds found.';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Calculate column widths
|
|
16
|
+
const rows = uploads.map(u => ({
|
|
17
|
+
id: (u.id || '').substring(0, 8),
|
|
18
|
+
platform: u.platform || '—',
|
|
19
|
+
app: u.app_name || '—',
|
|
20
|
+
version: u.version || '—',
|
|
21
|
+
build: u.build_number || '—',
|
|
22
|
+
profile: u.profile || '—',
|
|
23
|
+
uploaded: formatDate(u.uploaded_at || u.created_at),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// Build table
|
|
27
|
+
const header = 'ID PLATFORM APP VERSION BUILD PROFILE UPLOADED';
|
|
28
|
+
const separator = '─'.repeat(header.length);
|
|
29
|
+
|
|
30
|
+
const lines = [header, separator];
|
|
31
|
+
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
const line = [
|
|
34
|
+
row.id.padEnd(8),
|
|
35
|
+
row.platform.padEnd(8),
|
|
36
|
+
row.app.substring(0, 13).padEnd(13),
|
|
37
|
+
row.version.substring(0, 10).padEnd(10),
|
|
38
|
+
row.build.substring(0, 6).padEnd(6),
|
|
39
|
+
row.profile.substring(0, 12).padEnd(12),
|
|
40
|
+
row.uploaded,
|
|
41
|
+
].join(' ');
|
|
42
|
+
lines.push(line);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatInfo(upload, jsonMode = false) {
|
|
49
|
+
if (jsonMode) {
|
|
50
|
+
return JSON.stringify(upload, null, 2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lines = [
|
|
54
|
+
`ID: ${upload.id}`,
|
|
55
|
+
`Platform: ${upload.platform || '—'}`,
|
|
56
|
+
`App Name: ${upload.app_name || '—'}`,
|
|
57
|
+
`Bundle ID: ${upload.bundle_id || '—'}`,
|
|
58
|
+
`Version: ${upload.version || '—'}`,
|
|
59
|
+
`Build Number: ${upload.build_number || '—'}`,
|
|
60
|
+
`Profile: ${upload.profile || '—'}`,
|
|
61
|
+
`Uploaded: ${formatDate(upload.uploaded_at || upload.created_at)}`,
|
|
62
|
+
`Page URL: ${upload.pageUrl || '—'}`,
|
|
63
|
+
`Download URL: ${upload.downloadUrl || '—'}`,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatUpload(result, jsonMode = false) {
|
|
70
|
+
if (jsonMode) {
|
|
71
|
+
return JSON.stringify(result, null, 2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const lines = [
|
|
75
|
+
'✅ Upload successful!',
|
|
76
|
+
'',
|
|
77
|
+
`Build ID: ${result.id}`,
|
|
78
|
+
`Page URL: ${result.page_url}`,
|
|
79
|
+
`Install URL: ${result.install_url}`,
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatDelete(jsonMode = false) {
|
|
86
|
+
if (jsonMode) {
|
|
87
|
+
return JSON.stringify({ ok: true }, null, 2);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return '✅ Build deleted successfully';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatDate(dateStr) {
|
|
94
|
+
if (!dateStr) return '—';
|
|
95
|
+
try {
|
|
96
|
+
const date = new Date(dateStr);
|
|
97
|
+
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
|
98
|
+
} catch {
|
|
99
|
+
return '—';
|
|
100
|
+
}
|
|
101
|
+
}
|