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 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,6 +1,6 @@
1
1
  {
2
2
  "name": "sloss-cli",
3
- "version": "1.2.0",
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": {
@@ -1,171 +1,136 @@
1
1
  ---
2
2
  name: sloss
3
- description: "Build, distribute, and manage Expo app builds (iOS & Android) via the Sloss CLI. Use when queuing builds, listing builds, uploading artifacts, checking build status, or managing the Sloss build server."
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 — Build Distribution for Expo Apps
6
+ # Sloss — IPA/APK Distribution Server
9
7
 
10
- Sloss is a self-hosted build distribution server for Expo apps (iOS & Android). The `sloss` CLI queues builds, uploads artifacts, and manages builds from the terminal.
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
- ## Prerequisites
10
+ ## Repos
13
11
 
14
- 1. `sloss` CLI installed (`sloss --version` to verify)
15
- 2. A Sloss server instance (self-hosted)
16
- 3. An account on the Sloss server with an API key
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
- ## Authentication
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
- ```bash
21
- # Login once — saves API key to ~/.config/sloss/credentials.json
22
- sloss login
23
- ```
19
+ ## Server Setup
24
20
 
25
- API key resolution (highest → lowest priority):
26
- 1. `--api-key` flag
27
- 2. `SLOSS_API_KEY` environment variable
28
- 3. `~/.config/sloss/credentials.json`
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
- ## Queue a Build
36
-
37
- Run from the Expo project root (requires `.sloss.json` config file):
27
+ ## Server Status & Restart
38
28
 
39
29
  ```bash
40
- # iOS development build (default)
41
- sloss build
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
- # Production build with minor version bump
47
- sloss build --profile production --bump minor
33
+ # View logs
34
+ tail -f /tmp/sloss.log
48
35
 
49
- # Build from a different directory
50
- sloss build --dir /path/to/expo/project
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
- ### Build options
41
+ ## CLI Usage
54
42
 
55
- | Flag | Default | Values |
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
- ### Build flow
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 list
75
- sloss list --limit 20
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
- ## Upload a Pre-Built Artifact
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
- If you already have an IPA or APK:
58
+ ### Commands
87
59
 
88
60
  ```bash
89
- # Upload an IPA
90
- sloss upload ./App.ipa --platform ios --profile preview
91
-
92
- # Upload an APK
93
- sloss upload ./app.apk --platform android --profile development
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
- Upload options:
80
+ ### Build Flow
97
81
 
98
- | Flag | Description |
99
- |------|-------------|
100
- | `--platform` | **Required.** `ios` or `android` |
101
- | `--profile` | `development`, `preview`, or `production` |
102
- | `--app-name` | App display name |
103
- | `--version` | Version string |
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
- ## Delete a Build
89
+ ## Manual Upload via curl
107
90
 
108
91
  ```bash
109
- sloss delete <build-id>
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
- ## Project Configuration
113
-
114
- ### `.sloss.json`
102
+ **profile** values: `development` Ā· `preview` Ā· `production`
115
103
 
116
- Place in the Expo project root:
104
+ ## Install on Device
117
105
 
118
- ```json
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
- ### `app.version.json`
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
- Reference this in `app.config.js` so Expo picks it up:
142
-
143
- ```js
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
- ## Global Flags
116
+ ## API Endpoints
155
117
 
156
- | Flag | Description |
157
- |------|-------------|
158
- | `--api-key <key>` | Override API key |
159
- | `--url <url>` | Override server URL |
160
- | `--json` | JSON output |
161
- | `--version` | Show CLI version |
162
- | `--help` | Show help |
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
- ## Output Formats
130
+ ## Build Agent
165
131
 
166
- All commands support `--json` for structured JSON output:
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
- ```bash
169
- sloss list --json
170
- sloss info <build-id> --json
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
+ }