sloss-cli 1.1.0 → 1.2.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 +173 -36
- package/bin/sloss.js +16 -0
- package/package.json +15 -4
- package/skills/sloss/SKILL.md +136 -0
- package/src/commands/init.js +290 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# sloss-cli
|
|
2
2
|
|
|
3
|
-
Command-line interface for [Sloss](https://github.com/aualdrich/sloss) — a self-hosted
|
|
3
|
+
Command-line interface for [Sloss](https://github.com/aualdrich/sloss) — a self-hosted build distribution server for Expo apps. Supports iOS and Android.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -11,43 +11,167 @@ npm install -g sloss-cli
|
|
|
11
11
|
Or as a project dependency:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install sloss-cli
|
|
14
|
+
npm install --save-dev sloss-cli
|
|
15
15
|
# or
|
|
16
|
-
bun add sloss-cli
|
|
16
|
+
bun add --dev sloss-cli
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
### 1. Create a Sloss account
|
|
22
|
+
|
|
23
|
+
Sign up at your team's Sloss instance (e.g. `https://sloss.example.com/signup`). After signing up, you'll receive an API key on the onboarding page. You can also find it under **Settings → API Key**.
|
|
24
|
+
|
|
25
|
+
### 2. Authenticate the CLI
|
|
20
26
|
|
|
21
27
|
```bash
|
|
22
|
-
# Authenticate (saves API key to ~/.config/sloss/credentials.json)
|
|
23
28
|
sloss login
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This prompts for your email and password, fetches your API key, and saves it to `~/.config/sloss/credentials.json`. You only need to do this once per machine.
|
|
32
|
+
|
|
33
|
+
Alternatively, set the `SLOSS_API_KEY` environment variable or pass `--api-key` to any command.
|
|
34
|
+
|
|
35
|
+
### 3. Add `.sloss.json` to your Expo project
|
|
36
|
+
|
|
37
|
+
Create a `.sloss.json` file in your Expo project root (next to `app.json`):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"type": "expo",
|
|
42
|
+
"app_name": "MyApp",
|
|
43
|
+
"bundle_id": "com.mycompany.myapp",
|
|
44
|
+
"version_file": "app.version.json",
|
|
45
|
+
"profiles": {
|
|
46
|
+
"development": {
|
|
47
|
+
"bundle_id_suffix": ".dev"
|
|
48
|
+
},
|
|
49
|
+
"preview": {
|
|
50
|
+
"bundle_id_suffix": ".preview"
|
|
51
|
+
},
|
|
52
|
+
"production": {
|
|
53
|
+
"bundle_id_suffix": ""
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Field | Description |
|
|
60
|
+
|-------|-------------|
|
|
61
|
+
| `type` | Always `"expo"` |
|
|
62
|
+
| `app_name` | Display name for the build on Sloss |
|
|
63
|
+
| `bundle_id` | Base bundle identifier |
|
|
64
|
+
| `version_file` | Path to a JSON file containing `version` and `buildNumber` (relative to project root) |
|
|
65
|
+
| `profiles` | Build profile config — `bundle_id_suffix` is appended to `bundle_id` per profile |
|
|
66
|
+
|
|
67
|
+
### 4. Create `app.version.json`
|
|
68
|
+
|
|
69
|
+
Sloss reads version info from a dedicated file so the build agent can bump versions independently:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"version": "1.0.0",
|
|
74
|
+
"buildNumber": "1"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Reference this in your `app.json` / `app.config.js` so Expo picks it up:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
// app.config.js
|
|
82
|
+
const version = require('./app.version.json');
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
expo: {
|
|
86
|
+
version: version.version,
|
|
87
|
+
ios: { buildNumber: String(version.buildNumber) },
|
|
88
|
+
android: { versionCode: parseInt(version.buildNumber, 10) },
|
|
89
|
+
// ... rest of your config
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Usage
|
|
95
|
+
|
|
96
|
+
### Queue a build
|
|
97
|
+
|
|
98
|
+
From your Expo project directory:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# iOS development build (default)
|
|
102
|
+
sloss build
|
|
103
|
+
|
|
104
|
+
# Android preview build
|
|
105
|
+
sloss build --platform android --profile preview
|
|
106
|
+
|
|
107
|
+
# Production build with minor version bump
|
|
108
|
+
sloss build --profile production --bump minor
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The CLI packages your project via `git archive`, uploads it to Sloss, and a build agent picks it up. You'll get a link to the build page with live logs.
|
|
112
|
+
|
|
113
|
+
#### Build options
|
|
114
|
+
|
|
115
|
+
| Flag | Default | Description |
|
|
116
|
+
|------|---------|-------------|
|
|
117
|
+
| `--platform <platform>` | `ios` | `ios` or `android` |
|
|
118
|
+
| `--profile <profile>` | `development` | `development`, `preview`, or `production` |
|
|
119
|
+
| `--bump <type>` | `patch` | Version bump for production builds: `patch`, `minor`, or `major` |
|
|
120
|
+
| `--dir <path>` | `.` | Project directory (if not running from project root) |
|
|
24
121
|
|
|
25
|
-
|
|
122
|
+
### List builds
|
|
123
|
+
|
|
124
|
+
```bash
|
|
26
125
|
sloss list
|
|
126
|
+
sloss list --limit 20
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Get build details
|
|
27
130
|
|
|
131
|
+
```bash
|
|
132
|
+
sloss info <build-id>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Upload a pre-built artifact
|
|
136
|
+
|
|
137
|
+
If you already have an IPA or APK (e.g. from a local build):
|
|
138
|
+
|
|
139
|
+
```bash
|
|
28
140
|
# Upload an IPA
|
|
29
|
-
sloss upload
|
|
141
|
+
sloss upload ./build/MyApp.ipa --platform ios --profile preview
|
|
30
142
|
|
|
31
143
|
# Upload an APK
|
|
32
|
-
sloss upload
|
|
144
|
+
sloss upload ./build/app-release.apk --platform android --profile production
|
|
145
|
+
```
|
|
33
146
|
|
|
34
|
-
|
|
35
|
-
sloss info <build-id>
|
|
147
|
+
#### Upload options
|
|
36
148
|
|
|
37
|
-
|
|
149
|
+
| Flag | Description |
|
|
150
|
+
|------|-------------|
|
|
151
|
+
| `--platform <platform>` | **Required.** `ios` or `android` |
|
|
152
|
+
| `--profile <profile>` | Build profile: `development`, `preview`, or `production` |
|
|
153
|
+
| `--app-name <name>` | App display name |
|
|
154
|
+
| `--version <version>` | Version string |
|
|
155
|
+
| `--build-number <number>` | Build number |
|
|
156
|
+
|
|
157
|
+
### Delete a build
|
|
158
|
+
|
|
159
|
+
```bash
|
|
38
160
|
sloss delete <build-id>
|
|
39
161
|
```
|
|
40
162
|
|
|
41
163
|
## Authentication
|
|
42
164
|
|
|
43
165
|
API key resolution order (highest → lowest priority):
|
|
166
|
+
|
|
44
167
|
1. `--api-key` CLI flag
|
|
45
168
|
2. `SLOSS_API_KEY` environment variable
|
|
46
169
|
3. `~/.config/sloss/credentials.json` (saved by `sloss login`)
|
|
47
170
|
|
|
48
171
|
## Server URL
|
|
49
172
|
|
|
50
|
-
URL resolution order:
|
|
173
|
+
URL resolution order (highest → lowest priority):
|
|
174
|
+
|
|
51
175
|
1. `--url` CLI flag
|
|
52
176
|
2. `SLOSS_URL` environment variable
|
|
53
177
|
3. `~/.config/sloss/credentials.json`
|
|
@@ -55,40 +179,53 @@ URL resolution order:
|
|
|
55
179
|
|
|
56
180
|
## Global Options
|
|
57
181
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
182
|
+
| Flag | Description |
|
|
183
|
+
|------|-------------|
|
|
184
|
+
| `--api-key <key>` | Override API key |
|
|
185
|
+
| `--url <url>` | Override server URL |
|
|
186
|
+
| `--json` | Output as JSON |
|
|
187
|
+
| `--version` | Show CLI version |
|
|
188
|
+
| `--help` | Show help |
|
|
189
|
+
|
|
190
|
+
## How It Works
|
|
191
|
+
|
|
192
|
+
1. `sloss build` reads `.sloss.json` from your project root
|
|
193
|
+
2. Your committed source is packaged into a tarball via `git archive`
|
|
194
|
+
3. The tarball is uploaded to your Sloss server
|
|
195
|
+
4. A build agent picks up the job and builds it locally using Xcode (iOS) or Gradle (Android)
|
|
196
|
+
5. The finished IPA/APK is uploaded back to Sloss
|
|
197
|
+
6. You get a build page with install links and QR codes for on-device installation
|
|
198
|
+
|
|
199
|
+
## OpenClaw Skill
|
|
200
|
+
|
|
201
|
+
This package includes an [OpenClaw](https://openclaw.ai) skill so your AI agent can use the Sloss CLI. After installing `sloss-cli`, add the skill directory to your OpenClaw config:
|
|
202
|
+
|
|
203
|
+
```json5
|
|
204
|
+
// ~/.openclaw/openclaw.json
|
|
205
|
+
{
|
|
206
|
+
skills: {
|
|
207
|
+
load: {
|
|
208
|
+
extraDirs: ["./node_modules/sloss-cli/skills"]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
64
212
|
```
|
|
65
213
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
Prerequisites:
|
|
69
|
-
- An [npm](https://www.npmjs.com) account with publish access
|
|
70
|
-
- An npm access token (Granular token with "Bypass 2FA" enabled, or an Automation classic token)
|
|
214
|
+
Or copy the skill into your workspace:
|
|
71
215
|
|
|
72
216
|
```bash
|
|
73
|
-
|
|
74
|
-
echo "//registry.npmjs.org/:_authToken=YOUR_TOKEN" > ~/.npmrc
|
|
75
|
-
|
|
76
|
-
# Bump version (patch/minor/major)
|
|
77
|
-
npm version patch
|
|
78
|
-
|
|
79
|
-
# Publish
|
|
80
|
-
npm publish --access public
|
|
217
|
+
cp -r ./node_modules/sloss-cli/skills/sloss <workspace>/skills/sloss
|
|
81
218
|
```
|
|
82
219
|
|
|
83
|
-
The
|
|
220
|
+
The skill is also available on [ClawHub](https://clawhub.com):
|
|
84
221
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
222
|
+
```bash
|
|
223
|
+
clawhub install sloss
|
|
224
|
+
```
|
|
88
225
|
|
|
89
226
|
## Related
|
|
90
227
|
|
|
91
|
-
- **[Sloss Server](https://github.com/aualdrich/sloss)** — The distribution server
|
|
228
|
+
- **[Sloss Server](https://github.com/aualdrich/sloss)** — The self-hosted distribution server
|
|
92
229
|
|
|
93
230
|
## License
|
|
94
231
|
|
package/bin/sloss.js
CHANGED
|
@@ -12,6 +12,7 @@ import { uploadCommand } from '../src/commands/upload.js';
|
|
|
12
12
|
import { infoCommand } from '../src/commands/info.js';
|
|
13
13
|
import { deleteCommand } from '../src/commands/delete.js';
|
|
14
14
|
import { buildCommand } from '../src/commands/build.js';
|
|
15
|
+
import { initCommand } from '../src/commands/init.js';
|
|
15
16
|
|
|
16
17
|
const program = new Command();
|
|
17
18
|
|
|
@@ -23,6 +24,21 @@ program
|
|
|
23
24
|
.option('--url <url>', 'Server URL (overrides SLOSS_URL env var)')
|
|
24
25
|
.option('--json', 'Output as JSON instead of human-readable format');
|
|
25
26
|
|
|
27
|
+
// Init command
|
|
28
|
+
program
|
|
29
|
+
.command('init')
|
|
30
|
+
.description('Set up a project for Sloss builds')
|
|
31
|
+
.option('--dir <path>', 'Project directory (default: current directory)')
|
|
32
|
+
.option('--url <url>', 'Server URL')
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
try {
|
|
35
|
+
await initCommand({ ...options, url: program.opts().url || options.url });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(`Error: ${error.message}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
26
42
|
// Login command
|
|
27
43
|
program
|
|
28
44
|
.command('login')
|
package/package.json
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sloss-cli",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "CLI for Sloss — a self-hosted IPA/APK distribution server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"sloss": "
|
|
7
|
+
"sloss": "bin/sloss.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
11
|
"src/",
|
|
12
|
+
"skills/",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"sloss",
|
|
17
|
+
"ios",
|
|
18
|
+
"android",
|
|
19
|
+
"ipa",
|
|
20
|
+
"apk",
|
|
21
|
+
"build",
|
|
22
|
+
"distribution",
|
|
23
|
+
"testflight",
|
|
24
|
+
"diawi"
|
|
25
|
+
],
|
|
15
26
|
"author": "Front Porch Software",
|
|
16
27
|
"license": "MIT",
|
|
17
28
|
"repository": {
|
|
18
29
|
"type": "git",
|
|
19
|
-
"url": "https://github.com/aualdrich/sloss-cli.git"
|
|
30
|
+
"url": "git+https://github.com/aualdrich/sloss-cli.git"
|
|
20
31
|
},
|
|
21
32
|
"engines": {
|
|
22
33
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sloss
|
|
3
|
+
description: Manage Sloss — the local IPA/APK distribution server for Switchboard. Use when Austin asks to check Sloss status, restart it, view builds, get an install link, or upload an IPA manually.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sloss — IPA/APK Distribution Server
|
|
7
|
+
|
|
8
|
+
Sloss is a lightweight self-hosted build distribution server, inspired by Diawi. Named after [Sloss Furnaces](https://www.slossfurnaces.com/) in Birmingham, AL.
|
|
9
|
+
|
|
10
|
+
## Repos
|
|
11
|
+
|
|
12
|
+
| Repo | Purpose | Local path |
|
|
13
|
+
|------|---------|------------|
|
|
14
|
+
| [aualdrich/sloss](https://github.com/aualdrich/sloss) | Build server (Node.js/Express) | `~/sloss/` |
|
|
15
|
+
| [aualdrich/sloss-cli](https://github.com/aualdrich/sloss-cli) | CLI (`sloss` command) | `~/sloss/cli/` |
|
|
16
|
+
|
|
17
|
+
> **Note:** `cli/` is a separate repo cloned inside `~/sloss/` for convenience. It is **not** tracked by the sloss server repo (gitignored). Always commit/push CLI changes from `~/sloss/cli/`.
|
|
18
|
+
|
|
19
|
+
## Server Setup
|
|
20
|
+
|
|
21
|
+
- **Port:** `3001`
|
|
22
|
+
- **Public URL:** `https://sloss.ngrok.app` (via ngrok LaunchAgent)
|
|
23
|
+
- **LaunchAgent:** `com.switchboard.sloss` — auto-starts at login, KeepAlive
|
|
24
|
+
- **Log:** `/tmp/sloss.log`
|
|
25
|
+
- **Auth:** Cognito JWT (web UI) + per-user API keys (CLI/API)
|
|
26
|
+
|
|
27
|
+
## Server Status & Restart
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Check if running
|
|
31
|
+
curl -s http://localhost:3001/ | head -3
|
|
32
|
+
|
|
33
|
+
# View logs
|
|
34
|
+
tail -f /tmp/sloss.log
|
|
35
|
+
|
|
36
|
+
# Restart
|
|
37
|
+
launchctl unload ~/Library/LaunchAgents/com.switchboard.sloss.plist
|
|
38
|
+
launchctl load ~/Library/LaunchAgents/com.switchboard.sloss.plist
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CLI Usage
|
|
42
|
+
|
|
43
|
+
The `sloss` CLI lives in `~/sloss/cli/`. To use it from anywhere, run `node ~/sloss/cli/bin/sloss.js` or install it globally.
|
|
44
|
+
|
|
45
|
+
### Authentication
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Login once — saves API key to ~/.config/sloss/credentials.json
|
|
49
|
+
sloss login
|
|
50
|
+
# → prompts for email + password, fetches API key, saves credentials
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
API key resolution order (highest → lowest priority):
|
|
54
|
+
1. `--api-key` flag
|
|
55
|
+
2. `SLOSS_API_KEY` env var
|
|
56
|
+
3. `~/.config/sloss/credentials.json`
|
|
57
|
+
|
|
58
|
+
### Commands
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
sloss login # Authenticate + save credentials
|
|
62
|
+
sloss build [options] # Queue a build via the Sloss build agent
|
|
63
|
+
sloss list [--limit 20] # List recent builds
|
|
64
|
+
sloss info <build-id> # Details for a specific build
|
|
65
|
+
sloss upload <file> --platform ios # Upload an IPA or APK manually
|
|
66
|
+
sloss delete <build-id> # Delete a build
|
|
67
|
+
|
|
68
|
+
# Build options
|
|
69
|
+
sloss build --profile development # Build profile (development|preview|production)
|
|
70
|
+
sloss build --platform ios # Platform (ios|android), default: ios
|
|
71
|
+
sloss build --bump minor # Version bump type (patch|minor|major), default: patch
|
|
72
|
+
sloss build --dir /path/to/project # Project directory, default: current dir
|
|
73
|
+
|
|
74
|
+
# Global flags (all commands)
|
|
75
|
+
sloss --url <url> <command> # Override server URL
|
|
76
|
+
sloss --api-key <key> <command> # Override API key
|
|
77
|
+
sloss --json <command> # JSON output
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Build Flow
|
|
81
|
+
|
|
82
|
+
1. CLI reads `.sloss.json` from the project root
|
|
83
|
+
2. Tars up committed source via `git archive` (respects `.gitignore`)
|
|
84
|
+
3. Uploads tarball to `POST /api/builds/start` with your per-user API key
|
|
85
|
+
4. Build agent (mac-mini) picks up the job, extracts, builds locally via Xcode
|
|
86
|
+
5. Agent uploads the IPA/APK artifact to Sloss when done
|
|
87
|
+
6. Build page with live logs at `https://sloss.ngrok.app/builds/<id>`
|
|
88
|
+
|
|
89
|
+
## Manual Upload via curl
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
curl -s -X POST https://sloss.ngrok.app/upload \
|
|
93
|
+
-H "Authorization: Bearer <your-api-key>" \
|
|
94
|
+
-F "ipa=@/path/to/App.ipa" \
|
|
95
|
+
-F "app_name=Switchboard" \
|
|
96
|
+
-F "bundle_id=com.aualdrich1.switchboard.preview" \
|
|
97
|
+
-F "version=1.0.0" \
|
|
98
|
+
-F "build_number=36" \
|
|
99
|
+
-F "profile=preview"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**profile** values: `development` · `preview` · `production`
|
|
103
|
+
|
|
104
|
+
## Install on Device
|
|
105
|
+
|
|
106
|
+
After a build, Sloss outputs:
|
|
107
|
+
```
|
|
108
|
+
🏭 Sloss build page : https://sloss.ngrok.app/b/<uuid>
|
|
109
|
+
📲 Install URL : itms-services://...
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
To install on iPhone:
|
|
113
|
+
1. Open the `itms-services://...` URL in **Safari** (not Chrome), or
|
|
114
|
+
2. Open the build page URL in Safari and tap **Install**
|
|
115
|
+
|
|
116
|
+
## API Endpoints
|
|
117
|
+
|
|
118
|
+
| Method | Path | Auth | Description |
|
|
119
|
+
|--------|-----------------------|------|------------------------------|
|
|
120
|
+
| POST | `/upload` | Yes | Upload an IPA or APK |
|
|
121
|
+
| GET | `/b/:id` | No | Build landing page |
|
|
122
|
+
| GET | `/m/:id` | No | Apple OTA manifest plist |
|
|
123
|
+
| GET | `/ipa/:id` | No | Raw IPA download |
|
|
124
|
+
| GET | `/` | No | All builds, newest first |
|
|
125
|
+
| POST | `/auth/login` | No | Login → Cognito JWT |
|
|
126
|
+
| GET | `/api-keys` | JWT | Get current user's API key |
|
|
127
|
+
| POST | `/api-keys/generate` | JWT | Generate new API key |
|
|
128
|
+
| POST | `/api-keys/regenerate`| JWT | Replace existing API key |
|
|
129
|
+
|
|
130
|
+
## Build Agent
|
|
131
|
+
|
|
132
|
+
The Sloss build agent runs as a LaunchAgent (`com.switchboard.sloss-agent`) on the Mac Mini, polling for queued jobs every 10s. It extracts the tarball, runs `eas build --local`, and uploads the resulting IPA/APK back to Sloss.
|
|
133
|
+
|
|
134
|
+
- **LaunchAgent:** `com.switchboard.sloss-agent`
|
|
135
|
+
- **Log:** `/Users/administrator/Library/Logs/sloss-agent.log`
|
|
136
|
+
- **Agent code:** `~/sloss/agent/`
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command - Set up a project for Sloss builds
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Check if user is logged in, prompt login if not
|
|
6
|
+
* 2. Detect project type (Expo, React Native, etc.)
|
|
7
|
+
* 3. Auto-detect app name and bundle ID from project config
|
|
8
|
+
* 4. Prompt user to confirm / override detected values
|
|
9
|
+
* 5. Generate .sloss.json with conventional defaults
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { homedir } from 'os';
|
|
15
|
+
import { createInterface } from 'readline';
|
|
16
|
+
import { loginCommand } from './login.js';
|
|
17
|
+
|
|
18
|
+
const CREDENTIALS_PATH = join(homedir(), '.config', 'sloss', 'credentials.json');
|
|
19
|
+
|
|
20
|
+
function prompt(question, defaultValue = '') {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const rl = createInterface({
|
|
23
|
+
input: process.stdin,
|
|
24
|
+
output: process.stdout,
|
|
25
|
+
});
|
|
26
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
27
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
28
|
+
rl.close();
|
|
29
|
+
resolve(answer.trim() || defaultValue);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function promptYesNo(question, defaultYes = true) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const rl = createInterface({
|
|
37
|
+
input: process.stdin,
|
|
38
|
+
output: process.stdout,
|
|
39
|
+
});
|
|
40
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
41
|
+
rl.question(`${question} (${hint}): `, (answer) => {
|
|
42
|
+
rl.close();
|
|
43
|
+
const val = answer.trim().toLowerCase();
|
|
44
|
+
if (val === '') resolve(defaultYes);
|
|
45
|
+
else resolve(val === 'y' || val === 'yes');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detect project type and extract app metadata from config files.
|
|
52
|
+
*/
|
|
53
|
+
function detectProject(projectDir) {
|
|
54
|
+
const detected = {
|
|
55
|
+
type: null,
|
|
56
|
+
appName: null,
|
|
57
|
+
bundleId: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Try Expo: app.json
|
|
61
|
+
const appJsonPath = join(projectDir, 'app.json');
|
|
62
|
+
if (existsSync(appJsonPath)) {
|
|
63
|
+
try {
|
|
64
|
+
const appJson = JSON.parse(readFileSync(appJsonPath, 'utf8'));
|
|
65
|
+
const expo = appJson.expo || appJson;
|
|
66
|
+
detected.type = 'expo';
|
|
67
|
+
detected.appName = expo.name || null;
|
|
68
|
+
detected.bundleId = expo.ios?.bundleIdentifier || expo.android?.package || null;
|
|
69
|
+
} catch { /* ignore parse errors */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Try Expo: app.config.js (parse statically — look for common patterns)
|
|
73
|
+
if (!detected.bundleId) {
|
|
74
|
+
const appConfigJsPath = join(projectDir, 'app.config.js');
|
|
75
|
+
if (existsSync(appConfigJsPath)) {
|
|
76
|
+
try {
|
|
77
|
+
const src = readFileSync(appConfigJsPath, 'utf8');
|
|
78
|
+
detected.type = 'expo';
|
|
79
|
+
|
|
80
|
+
// Extract base app name (the production one, no Dev/Preview suffix)
|
|
81
|
+
const allNames = [...src.matchAll(/return\s+['"]([^'"]+)['"]/g)]
|
|
82
|
+
.map(m => m[1])
|
|
83
|
+
.filter(n => !n.includes('/') && !n.includes('.') && n.length < 50);
|
|
84
|
+
const prodName = allNames.find(n => !/ Dev$| Preview$| Staging$/i.test(n))
|
|
85
|
+
|| allNames[allNames.length - 1];
|
|
86
|
+
if (prodName && !detected.appName) {
|
|
87
|
+
detected.appName = prodName;
|
|
88
|
+
}
|
|
89
|
+
if (!detected.appName) {
|
|
90
|
+
const nameMatch = src.match(/name:\s*['"]([^'"]+)['"]/m);
|
|
91
|
+
if (nameMatch) detected.appName = nameMatch[1];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Extract base bundle ID (production, no suffix)
|
|
95
|
+
const bundleMatch = src.match(/return\s+['"]([a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]*){2,})['"]/m)
|
|
96
|
+
|| src.match(/bundleIdentifier.*?['"]([a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]*){2,})['"]/m);
|
|
97
|
+
if (bundleMatch) {
|
|
98
|
+
// Try to find the base (production) bundle ID — the one without .dev or .preview
|
|
99
|
+
const allBundles = [...src.matchAll(/['"]([a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]*){2,})['"]/g)]
|
|
100
|
+
.map(m => m[1])
|
|
101
|
+
.filter(id => !id.endsWith('.dev') && !id.endsWith('.preview'));
|
|
102
|
+
detected.bundleId = allBundles[0] || bundleMatch[1];
|
|
103
|
+
}
|
|
104
|
+
} catch { /* ignore */ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Try eas.json for profile detection
|
|
109
|
+
const easJsonPath = join(projectDir, 'eas.json');
|
|
110
|
+
if (existsSync(easJsonPath)) {
|
|
111
|
+
detected.type = detected.type || 'expo';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Try React Native (no Expo): look for android/app/build.gradle or ios/*.xcodeproj
|
|
115
|
+
if (!detected.type) {
|
|
116
|
+
if (existsSync(join(projectDir, 'android', 'app', 'build.gradle')) ||
|
|
117
|
+
existsSync(join(projectDir, 'ios'))) {
|
|
118
|
+
detected.type = 'react-native';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Try package.json for app name fallback
|
|
123
|
+
if (!detected.appName) {
|
|
124
|
+
const pkgPath = join(projectDir, 'package.json');
|
|
125
|
+
if (existsSync(pkgPath)) {
|
|
126
|
+
try {
|
|
127
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
128
|
+
detected.appName = pkg.name || null;
|
|
129
|
+
} catch { /* ignore */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return detected;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Detect build profiles from eas.json
|
|
138
|
+
*/
|
|
139
|
+
function detectProfiles(projectDir) {
|
|
140
|
+
const easJsonPath = join(projectDir, 'eas.json');
|
|
141
|
+
if (!existsSync(easJsonPath)) return null;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const easJson = JSON.parse(readFileSync(easJsonPath, 'utf8'));
|
|
145
|
+
const build = easJson.build || {};
|
|
146
|
+
const profiles = {};
|
|
147
|
+
|
|
148
|
+
for (const [name, config] of Object.entries(build)) {
|
|
149
|
+
const env = config.env || {};
|
|
150
|
+
const variant = env.APP_VARIANT || name;
|
|
151
|
+
|
|
152
|
+
// Convention: dev → .dev suffix, preview → .preview, production → no suffix
|
|
153
|
+
let suffix = '';
|
|
154
|
+
if (variant === 'development' || variant === 'dev') suffix = '.dev';
|
|
155
|
+
else if (variant === 'preview' || variant === 'staging') suffix = '.preview';
|
|
156
|
+
|
|
157
|
+
profiles[name] = { bundle_id_suffix: suffix };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return Object.keys(profiles).length > 0 ? profiles : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function initCommand(options) {
|
|
167
|
+
const projectDir = options.dir || process.cwd();
|
|
168
|
+
|
|
169
|
+
console.log('');
|
|
170
|
+
console.log('🏭 Sloss — Project Setup');
|
|
171
|
+
console.log('════════════════════════════════════════');
|
|
172
|
+
console.log('');
|
|
173
|
+
|
|
174
|
+
// Check for existing .sloss.json
|
|
175
|
+
const slossPath = join(projectDir, '.sloss.json');
|
|
176
|
+
if (existsSync(slossPath)) {
|
|
177
|
+
const overwrite = await promptYesNo('.sloss.json already exists. Overwrite?', false);
|
|
178
|
+
if (!overwrite) {
|
|
179
|
+
console.log('Aborted.');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Step 1: Check login
|
|
185
|
+
let isLoggedIn = false;
|
|
186
|
+
try {
|
|
187
|
+
const creds = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
188
|
+
if (creds.apiKey) {
|
|
189
|
+
isLoggedIn = true;
|
|
190
|
+
console.log(`✅ Logged in as ${creds.email || 'unknown'}`);
|
|
191
|
+
}
|
|
192
|
+
} catch { /* not logged in */ }
|
|
193
|
+
|
|
194
|
+
if (!isLoggedIn) {
|
|
195
|
+
console.log('You need to log in to Sloss first.\n');
|
|
196
|
+
const baseUrl = options.url || 'https://sloss.ngrok.app';
|
|
197
|
+
await loginCommand({}, baseUrl);
|
|
198
|
+
console.log('');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Step 2: Detect project
|
|
202
|
+
console.log('🔍 Detecting project...');
|
|
203
|
+
const detected = detectProject(projectDir);
|
|
204
|
+
|
|
205
|
+
if (detected.type) {
|
|
206
|
+
console.log(` Found: ${detected.type} project`);
|
|
207
|
+
} else {
|
|
208
|
+
console.log(' Could not auto-detect project type.');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Step 3: Prompt for app name and bundle ID
|
|
212
|
+
console.log('');
|
|
213
|
+
const appName = await prompt('App name', detected.appName || '');
|
|
214
|
+
const bundleId = await prompt('Bundle ID', detected.bundleId || '');
|
|
215
|
+
|
|
216
|
+
if (!appName || !bundleId) {
|
|
217
|
+
console.error('\n❌ App name and bundle ID are required.');
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Step 4: Build .sloss.json with conventions
|
|
222
|
+
const profiles = detectProfiles(projectDir) || {
|
|
223
|
+
development: { bundle_id_suffix: '.dev' },
|
|
224
|
+
preview: { bundle_id_suffix: '.preview' },
|
|
225
|
+
production: { bundle_id_suffix: '' },
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const config = {
|
|
229
|
+
type: detected.type || 'expo',
|
|
230
|
+
app_name: appName,
|
|
231
|
+
bundle_id: bundleId,
|
|
232
|
+
version_file: 'app.version.json',
|
|
233
|
+
build_dir: '.',
|
|
234
|
+
ios_plist: 'ios/switchboard/Info.plist',
|
|
235
|
+
profiles,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Try to find the actual iOS plist path
|
|
239
|
+
const plistCandidates = [
|
|
240
|
+
`ios/${appName.toLowerCase()}/Info.plist`,
|
|
241
|
+
`ios/${appName.replace(/\s+/g, '')}/Info.plist`,
|
|
242
|
+
`ios/${appName}/Info.plist`,
|
|
243
|
+
];
|
|
244
|
+
for (const candidate of plistCandidates) {
|
|
245
|
+
if (existsSync(join(projectDir, candidate))) {
|
|
246
|
+
config.ios_plist = candidate;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// If no ios_plist found, try a glob
|
|
252
|
+
if (!existsSync(join(projectDir, config.ios_plist))) {
|
|
253
|
+
try {
|
|
254
|
+
const { readdirSync } = await import('fs');
|
|
255
|
+
const iosDir = join(projectDir, 'ios');
|
|
256
|
+
if (existsSync(iosDir)) {
|
|
257
|
+
const dirs = readdirSync(iosDir, { withFileTypes: true })
|
|
258
|
+
.filter(d => d.isDirectory() && !d.name.startsWith('.') && d.name !== 'Pods' && d.name !== 'build');
|
|
259
|
+
for (const dir of dirs) {
|
|
260
|
+
const plist = join('ios', dir.name, 'Info.plist');
|
|
261
|
+
if (existsSync(join(projectDir, plist))) {
|
|
262
|
+
config.ios_plist = plist;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch { /* ignore */ }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Ensure version file exists
|
|
271
|
+
const versionFilePath = join(projectDir, 'app.version.json');
|
|
272
|
+
if (!existsSync(versionFilePath)) {
|
|
273
|
+
const createVersionFile = await promptYesNo('app.version.json not found. Create it?', true);
|
|
274
|
+
if (createVersionFile) {
|
|
275
|
+
writeFileSync(versionFilePath, JSON.stringify({ version: '1.0.0', buildNumber: '1' }, null, 2) + '\n');
|
|
276
|
+
console.log(' Created app.version.json');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Write .sloss.json
|
|
281
|
+
writeFileSync(slossPath, JSON.stringify(config, null, 2) + '\n');
|
|
282
|
+
|
|
283
|
+
console.log('');
|
|
284
|
+
console.log('✅ Created .sloss.json');
|
|
285
|
+
console.log('');
|
|
286
|
+
console.log(' Next steps:');
|
|
287
|
+
console.log(' 1. Commit .sloss.json to your repo');
|
|
288
|
+
console.log(' 2. Run `sloss build` to queue your first build');
|
|
289
|
+
console.log('');
|
|
290
|
+
}
|