sloss-cli 1.2.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/bin/sloss.js +16 -0
- package/package.json +1 -1
- package/skills/sloss/SKILL.md +92 -127
- package/src/commands/init.js +290 -0
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
package/skills/sloss/SKILL.md
CHANGED
|
@@ -1,171 +1,136 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sloss
|
|
3
|
-
description:
|
|
4
|
-
homepage: https://github.com/aualdrich/sloss-cli
|
|
5
|
-
metadata: {"openclaw":{"emoji":"š","requires":{"bins":["sloss"]},"install":[{"id":"npm","kind":"node","package":"sloss-cli","bins":["sloss"],"label":"Install Sloss CLI (npm)"}]}}
|
|
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.
|
|
6
4
|
---
|
|
7
5
|
|
|
8
|
-
# Sloss ā
|
|
6
|
+
# Sloss ā IPA/APK Distribution Server
|
|
9
7
|
|
|
10
|
-
Sloss is a self-hosted build distribution server
|
|
8
|
+
Sloss is a lightweight self-hosted build distribution server, inspired by Diawi. Named after [Sloss Furnaces](https://www.slossfurnaces.com/) in Birmingham, AL.
|
|
11
9
|
|
|
12
|
-
##
|
|
10
|
+
## Repos
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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/` |
|
|
17
16
|
|
|
18
|
-
|
|
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/`.
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
# Login once ā saves API key to ~/.config/sloss/credentials.json
|
|
22
|
-
sloss login
|
|
23
|
-
```
|
|
19
|
+
## Server Setup
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Server URL resolution (highest ā lowest priority):
|
|
31
|
-
1. `--url` flag
|
|
32
|
-
2. `SLOSS_URL` environment variable
|
|
33
|
-
3. `~/.config/sloss/credentials.json`
|
|
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)
|
|
34
26
|
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
Run from the Expo project root (requires `.sloss.json` config file):
|
|
27
|
+
## Server Status & Restart
|
|
38
28
|
|
|
39
29
|
```bash
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Android preview build
|
|
44
|
-
sloss build --platform android --profile preview
|
|
30
|
+
# Check if running
|
|
31
|
+
curl -s http://localhost:3001/ | head -3
|
|
45
32
|
|
|
46
|
-
#
|
|
47
|
-
|
|
33
|
+
# View logs
|
|
34
|
+
tail -f /tmp/sloss.log
|
|
48
35
|
|
|
49
|
-
#
|
|
50
|
-
|
|
36
|
+
# Restart
|
|
37
|
+
launchctl unload ~/Library/LaunchAgents/com.switchboard.sloss.plist
|
|
38
|
+
launchctl load ~/Library/LaunchAgents/com.switchboard.sloss.plist
|
|
51
39
|
```
|
|
52
40
|
|
|
53
|
-
|
|
41
|
+
## CLI Usage
|
|
54
42
|
|
|
55
|
-
|
|
56
|
-
|------|---------|--------|
|
|
57
|
-
| `--platform` | `ios` | `ios`, `android` |
|
|
58
|
-
| `--profile` | `development` | `development`, `preview`, `production` |
|
|
59
|
-
| `--bump` | `patch` | `patch`, `minor`, `major` |
|
|
60
|
-
| `--dir` | `.` | Path to project root |
|
|
43
|
+
The `sloss` CLI lives in `~/sloss/cli/`. To use it from anywhere, run `node ~/sloss/cli/bin/sloss.js` or install it globally.
|
|
61
44
|
|
|
62
|
-
###
|
|
63
|
-
|
|
64
|
-
1. CLI reads `.sloss.json` from the project root
|
|
65
|
-
2. Source is packaged via `git archive` (respects `.gitignore`)
|
|
66
|
-
3. Tarball is uploaded to the Sloss server
|
|
67
|
-
4. A build agent picks up the job and builds locally (Xcode for iOS, Gradle for Android)
|
|
68
|
-
5. The finished IPA/APK is uploaded back to Sloss
|
|
69
|
-
6. A build page with live logs and install links is available on the server
|
|
70
|
-
|
|
71
|
-
## List Builds
|
|
45
|
+
### Authentication
|
|
72
46
|
|
|
73
47
|
```bash
|
|
74
|
-
sloss
|
|
75
|
-
sloss
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
## Build Details
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
sloss info <build-id>
|
|
48
|
+
# Login once ā saves API key to ~/.config/sloss/credentials.json
|
|
49
|
+
sloss login
|
|
50
|
+
# ā prompts for email + password, fetches API key, saves credentials
|
|
82
51
|
```
|
|
83
52
|
|
|
84
|
-
|
|
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`
|
|
85
57
|
|
|
86
|
-
|
|
58
|
+
### Commands
|
|
87
59
|
|
|
88
60
|
```bash
|
|
89
|
-
#
|
|
90
|
-
sloss
|
|
91
|
-
|
|
92
|
-
#
|
|
93
|
-
sloss upload
|
|
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
|
|
94
78
|
```
|
|
95
79
|
|
|
96
|
-
|
|
80
|
+
### Build Flow
|
|
97
81
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
| `--build-number` | Build number |
|
|
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>`
|
|
105
88
|
|
|
106
|
-
##
|
|
89
|
+
## Manual Upload via curl
|
|
107
90
|
|
|
108
91
|
```bash
|
|
109
|
-
|
|
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"
|
|
110
100
|
```
|
|
111
101
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
### `.sloss.json`
|
|
102
|
+
**profile** values: `development` Ā· `preview` Ā· `production`
|
|
115
103
|
|
|
116
|
-
|
|
104
|
+
## Install on Device
|
|
117
105
|
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
"type": "expo",
|
|
121
|
-
"app_name": "MyApp",
|
|
122
|
-
"bundle_id": "com.example.myapp",
|
|
123
|
-
"version_file": "app.version.json",
|
|
124
|
-
"profiles": {
|
|
125
|
-
"development": { "bundle_id_suffix": ".dev" },
|
|
126
|
-
"preview": { "bundle_id_suffix": ".preview" },
|
|
127
|
-
"production": { "bundle_id_suffix": "" }
|
|
128
|
-
}
|
|
129
|
-
}
|
|
106
|
+
After a build, Sloss outputs:
|
|
130
107
|
```
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
```json
|
|
135
|
-
{
|
|
136
|
-
"version": "1.0.0",
|
|
137
|
-
"buildNumber": "1"
|
|
138
|
-
}
|
|
108
|
+
š Sloss build page : https://sloss.ngrok.app/b/<uuid>
|
|
109
|
+
š² Install URL : itms-services://...
|
|
139
110
|
```
|
|
140
111
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const version = require('./app.version.json');
|
|
145
|
-
module.exports = {
|
|
146
|
-
expo: {
|
|
147
|
-
version: version.version,
|
|
148
|
-
ios: { buildNumber: String(version.buildNumber) },
|
|
149
|
-
android: { versionCode: parseInt(version.buildNumber, 10) },
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
```
|
|
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**
|
|
153
115
|
|
|
154
|
-
##
|
|
116
|
+
## API Endpoints
|
|
155
117
|
|
|
156
|
-
|
|
|
157
|
-
|
|
158
|
-
|
|
|
159
|
-
|
|
|
160
|
-
|
|
|
161
|
-
|
|
|
162
|
-
|
|
|
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 |
|
|
163
129
|
|
|
164
|
-
##
|
|
130
|
+
## Build Agent
|
|
165
131
|
|
|
166
|
-
|
|
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.
|
|
167
133
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
```
|
|
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
|
+
}
|