sloss-cli 1.2.2 → 1.3.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
@@ -120,15 +120,15 @@ program
120
120
  // Build command
121
121
  program
122
122
  .command('build')
123
- .description('Queue a build via the Sloss build agent')
123
+ .description('Run a local EAS build and upload to Sloss')
124
124
  .option('--platform <platform>', 'Platform (ios or android)', 'ios')
125
125
  .option('--profile <profile>', 'Build profile (development, preview, production)', 'development')
126
126
  .option('--bump <type>', 'Version bump type for production (patch, minor, major)', 'patch')
127
- .option('--dir <path>', 'Project directory (default: current directory)', '.')
127
+ .option('--dir <path>', 'Project directory (default: current directory)')
128
+ .option('--yes', 'Skip confirmation prompt')
128
129
  .action(async (options) => {
129
130
  try {
130
- const config = resolveConfig(program.opts());
131
- await buildCommand(options, config);
131
+ await buildCommand(options);
132
132
  } catch (error) {
133
133
  console.error(`Error: ${error.message}`);
134
134
  process.exit(1);
package/build.sh ADDED
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env bash
2
+ # sloss build — Local EAS build + Sloss upload for Expo apps
3
+ # Invoked by the `sloss build` Node CLI command.
4
+ set -euo pipefail
5
+
6
+ # ─── Defaults ───────────────────────────────────────────────────────────────
7
+ PLATFORM="ios"
8
+ PROFILE="development"
9
+ BUMP="patch"
10
+ PROJECT_DIR="$(pwd)"
11
+ YES=false
12
+
13
+ # ─── Flag parsing ───────────────────────────────────────────────────────────
14
+ while [[ $# -gt 0 ]]; do
15
+ case "$1" in
16
+ --platform) PLATFORM="$2"; shift 2 ;;
17
+ --profile) PROFILE="$2"; shift 2 ;;
18
+ --bump) BUMP="$2"; shift 2 ;;
19
+ --dir) PROJECT_DIR="$2"; shift 2 ;;
20
+ --yes|-y) YES=true; shift ;;
21
+ --help|-h)
22
+ echo "Usage: sloss build [options]"
23
+ echo ""
24
+ echo "Options:"
25
+ echo " --platform ios|android Target platform (default: ios)"
26
+ echo " --profile <name> EAS build profile (default: development)"
27
+ echo " --bump patch|minor|major Version bump type (default: patch)"
28
+ echo " --dir <path> Project directory (default: cwd)"
29
+ echo " --yes, -y Skip confirmation prompt"
30
+ echo " --help, -h Show this help"
31
+ exit 0
32
+ ;;
33
+ *) echo "Unknown flag: $1"; exit 1 ;;
34
+ esac
35
+ done
36
+
37
+ # ─── Helpers ────────────────────────────────────────────────────────────────
38
+ # Use node for all JSON parsing (jq not guaranteed).
39
+ json_get() {
40
+ # json_get <file> <key> — read a top-level string value
41
+ node -e "const f=require('fs').readFileSync('$1','utf8'); console.log(JSON.parse(f)['$2'])"
42
+ }
43
+
44
+ json_set() {
45
+ # json_set <file> <key> <value> — set a top-level string value, preserve formatting
46
+ node -e "
47
+ const fs=require('fs'), p='$1', k='$2', v='$3';
48
+ const obj=JSON.parse(fs.readFileSync(p,'utf8'));
49
+ obj[k]=v;
50
+ fs.writeFileSync(p, JSON.stringify(obj, null, 2)+'\n');
51
+ "
52
+ }
53
+
54
+ json_nested() {
55
+ # json_nested <file> <path.to.key> — read a nested value via dot path
56
+ node -e "
57
+ const fs=require('fs'), p='$1', keys='$2'.split('.');
58
+ let obj=JSON.parse(fs.readFileSync(p,'utf8'));
59
+ for(const k of keys) obj=obj[k];
60
+ console.log(obj);
61
+ "
62
+ }
63
+
64
+ plist_set() {
65
+ # plist_set <file> <key> <value> — update a value in an XML plist
66
+ /usr/libexec/PlistBuddy -c "Set :$2 $3" "$1" 2>/dev/null || \
67
+ sed -i '' "s|<key>$2</key>.*<string>[^<]*</string>|<key>$2</key>\n\t<string>$3</string>|" "$1"
68
+ }
69
+
70
+ bump_version() {
71
+ # bump_version <current> <patch|minor|major> → new version string
72
+ node -e "
73
+ const [M,m,p]='$1'.split('.').map(Number);
74
+ const t='$2';
75
+ if(t==='major') console.log((M+1)+'.0.0');
76
+ else if(t==='minor') console.log(M+'.'+(m+1)+'.0');
77
+ else console.log(M+'.'+m+'.'+(p+1));
78
+ "
79
+ }
80
+
81
+ # ─── Validate inputs ───────────────────────────────────────────────────────
82
+ if [[ "$PLATFORM" != "ios" && "$PLATFORM" != "android" ]]; then
83
+ echo "Error: --platform must be ios or android"; exit 1
84
+ fi
85
+ if [[ "$BUMP" != "patch" && "$BUMP" != "minor" && "$BUMP" != "major" ]]; then
86
+ echo "Error: --bump must be patch, minor, or major"; exit 1
87
+ fi
88
+
89
+ # ─── Read .sloss.json ──────────────────────────────────────────────────────
90
+ CONFIG="$PROJECT_DIR/.sloss.json"
91
+ if [[ ! -f "$CONFIG" ]]; then
92
+ echo "Error: No .sloss.json found in $PROJECT_DIR"
93
+ exit 1
94
+ fi
95
+
96
+ APP_NAME=$(json_get "$CONFIG" "app_name")
97
+ BUNDLE_ID=$(json_get "$CONFIG" "bundle_id")
98
+ VERSION_FILE="$PROJECT_DIR/$(json_get "$CONFIG" "version_file")"
99
+ IOS_PLIST="$PROJECT_DIR/$(json_get "$CONFIG" "ios_plist")"
100
+ BUNDLE_SUFFIX=$(json_nested "$CONFIG" "profiles.$PROFILE.bundle_id_suffix")
101
+
102
+ FULL_BUNDLE_ID="${BUNDLE_ID}${BUNDLE_SUFFIX}"
103
+
104
+ # ─── Read current version info ─────────────────────────────────────────────
105
+ CURRENT_VERSION=$(json_get "$VERSION_FILE" "version")
106
+ CURRENT_BUILD=$(json_get "$VERSION_FILE" "buildNumber")
107
+ NEW_BUILD=$((CURRENT_BUILD + 1))
108
+
109
+ # Marketing version bump (production only)
110
+ if [[ "$PROFILE" == "production" ]]; then
111
+ NEW_VERSION=$(bump_version "$CURRENT_VERSION" "$BUMP")
112
+ else
113
+ NEW_VERSION="$CURRENT_VERSION"
114
+ fi
115
+
116
+ # ─── Confirmation ───────────────────────────────────────────────────────────
117
+ echo ""
118
+ echo "╔══════════════════════════════════════════╗"
119
+ echo "║ Sloss Build Summary ║"
120
+ echo "╠══════════════════════════════════════════╣"
121
+ echo "║ App: $APP_NAME"
122
+ echo "║ Bundle ID: $FULL_BUNDLE_ID"
123
+ echo "║ Platform: $PLATFORM"
124
+ echo "║ Profile: $PROFILE"
125
+ echo "║ Version: $CURRENT_VERSION → $NEW_VERSION"
126
+ echo "║ Build: $CURRENT_BUILD → $NEW_BUILD"
127
+ echo "╚══════════════════════════════════════════╝"
128
+ echo ""
129
+
130
+ if [[ "$YES" != true ]]; then
131
+ read -r -p "Proceed? [y/N] " confirm
132
+ if [[ "$confirm" != [yY] && "$confirm" != [yY][eE][sS] ]]; then
133
+ echo "Aborted."
134
+ exit 0
135
+ fi
136
+ fi
137
+
138
+ # ─── Bump versions ─────────────────────────────────────────────────────────
139
+ echo "📦 Bumping build number: $CURRENT_BUILD → $NEW_BUILD"
140
+ json_set "$VERSION_FILE" "buildNumber" "$NEW_BUILD"
141
+
142
+ # Update Info.plist CFBundleVersion
143
+ if [[ -f "$IOS_PLIST" ]]; then
144
+ plist_set "$IOS_PLIST" "CFBundleVersion" "$NEW_BUILD"
145
+ fi
146
+
147
+ if [[ "$PROFILE" == "production" ]]; then
148
+ echo "📦 Bumping marketing version: $CURRENT_VERSION → $NEW_VERSION ($BUMP)"
149
+ json_set "$VERSION_FILE" "version" "$NEW_VERSION"
150
+ if [[ -f "$IOS_PLIST" ]]; then
151
+ plist_set "$IOS_PLIST" "CFBundleShortVersionString" "$NEW_VERSION"
152
+ fi
153
+ fi
154
+
155
+ # ─── Run EAS build ──────────────────────────────────────────────────────────
156
+ echo ""
157
+ echo "🔨 Starting EAS local build..."
158
+ echo " eas build --profile $PROFILE --platform $PLATFORM --local --non-interactive"
159
+ echo ""
160
+
161
+ BUILD_LOG=$(mktemp)
162
+ ARTIFACT_PATH=""
163
+
164
+ # Run build, tee output so user sees it and we can parse it
165
+ set +e
166
+ (cd "$PROJECT_DIR" && eas build --profile "$PROFILE" --platform "$PLATFORM" --local --non-interactive 2>&1) | tee "$BUILD_LOG"
167
+ BUILD_EXIT=${PIPESTATUS[0]}
168
+ set -e
169
+
170
+ if [[ $BUILD_EXIT -ne 0 ]]; then
171
+ echo ""
172
+ echo "❌ EAS build failed (exit code $BUILD_EXIT). Skipping upload."
173
+ rm -f "$BUILD_LOG"
174
+ exit 1
175
+ fi
176
+
177
+ # ─── Find artifact ──────────────────────────────────────────────────────────
178
+ # EAS outputs lines like:
179
+ # "Build artifact: /path/to/build-xxxx.ipa"
180
+ # or the artifact path on its own line ending in .ipa/.apk
181
+ if [[ "$PLATFORM" == "ios" ]]; then
182
+ EXT="ipa"
183
+ else
184
+ EXT="apk"
185
+ fi
186
+
187
+ ARTIFACT_PATH=$(grep -oE '/[^ ]+\.'$EXT "$BUILD_LOG" | tail -1 || true)
188
+
189
+ # Fallback: check common EAS output directory
190
+ if [[ -z "$ARTIFACT_PATH" || ! -f "$ARTIFACT_PATH" ]]; then
191
+ ARTIFACT_PATH=$(find "$PROJECT_DIR" -maxdepth 1 -name "*.$EXT" -newer "$VERSION_FILE" -print -quit 2>/dev/null || true)
192
+ fi
193
+
194
+ rm -f "$BUILD_LOG"
195
+
196
+ if [[ -z "$ARTIFACT_PATH" || ! -f "$ARTIFACT_PATH" ]]; then
197
+ echo ""
198
+ echo "⚠️ Could not locate build artifact. Check the build output above."
199
+ echo " Expected a .$EXT file."
200
+ exit 1
201
+ fi
202
+
203
+ echo ""
204
+ echo "✅ Build artifact: $ARTIFACT_PATH"
205
+
206
+ # ─── Upload to Sloss ───────────────────────────────────────────────────────
207
+ echo ""
208
+ echo "📤 Uploading to Sloss..."
209
+
210
+ # Resolve Sloss URL and API key
211
+ UPLOAD_URL="${SLOSS_URL:-https://sloss.ngrok.app}/upload"
212
+
213
+ if [[ -z "${SLOSS_API_KEY:-}" ]]; then
214
+ CREDS_FILE="$HOME/.config/sloss/credentials.json"
215
+ if [[ -f "$CREDS_FILE" ]]; then
216
+ SLOSS_API_KEY=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$CREDS_FILE','utf8')).api_key)")
217
+ else
218
+ echo "❌ No SLOSS_API_KEY env var and no ~/.config/sloss/credentials.json found."
219
+ echo " Artifact is at: $ARTIFACT_PATH"
220
+ exit 1
221
+ fi
222
+ fi
223
+
224
+ # Upload — field name is 'ipa' for iOS, 'apk' for Android
225
+ FIELD_NAME="$EXT"
226
+ set +e
227
+ UPLOAD_RESPONSE=$(curl -s -w "\n%{http_code}" \
228
+ -H "Authorization: Bearer $SLOSS_API_KEY" \
229
+ -F "${FIELD_NAME}=@${ARTIFACT_PATH}" \
230
+ -F "app_name=${APP_NAME}" \
231
+ -F "bundle_id=${FULL_BUNDLE_ID}" \
232
+ -F "version=${NEW_VERSION}" \
233
+ -F "build_number=${NEW_BUILD}" \
234
+ -F "profile=${PROFILE}" \
235
+ "$UPLOAD_URL")
236
+ UPLOAD_EXIT=$?
237
+ set -e
238
+
239
+ if [[ $UPLOAD_EXIT -ne 0 ]]; then
240
+ echo "❌ Upload failed (curl error)."
241
+ echo " Artifact is at: $ARTIFACT_PATH"
242
+ exit 1
243
+ fi
244
+
245
+ HTTP_CODE=$(echo "$UPLOAD_RESPONSE" | tail -1)
246
+ RESPONSE_BODY=$(echo "$UPLOAD_RESPONSE" | sed '$d')
247
+
248
+ if [[ "$HTTP_CODE" -lt 200 || "$HTTP_CODE" -ge 300 ]]; then
249
+ echo "❌ Upload failed (HTTP $HTTP_CODE)."
250
+ echo " $RESPONSE_BODY"
251
+ echo " Artifact is at: $ARTIFACT_PATH"
252
+ exit 1
253
+ fi
254
+
255
+ # ─── Print results ──────────────────────────────────────────────────────────
256
+ PAGE_URL=$(node -e "try{console.log(JSON.parse(process.argv[1]).page_url||'')}catch{console.log('')}" "$RESPONSE_BODY")
257
+ INSTALL_URL=$(node -e "try{console.log(JSON.parse(process.argv[1]).install_url||'')}catch{console.log('')}" "$RESPONSE_BODY")
258
+
259
+ echo ""
260
+ echo "╔══════════════════════════════════════════╗"
261
+ echo "║ 🎉 Build Complete! ║"
262
+ echo "╠══════════════════════════════════════════╣"
263
+ echo "║ App: $APP_NAME"
264
+ echo "║ Version: $NEW_VERSION ($NEW_BUILD)"
265
+ echo "║ Profile: $PROFILE"
266
+ echo "║ Platform: $PLATFORM"
267
+ echo "║ Artifact: $ARTIFACT_PATH"
268
+ if [[ -n "$PAGE_URL" ]]; then
269
+ echo "║ Page: $PAGE_URL"
270
+ fi
271
+ if [[ -n "$INSTALL_URL" ]]; then
272
+ echo "║ Install: $INSTALL_URL"
273
+ fi
274
+ echo "╚══════════════════════════════════════════╝"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sloss-cli",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "CLI for Sloss — a self-hosted IPA/APK distribution server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "bin/",
11
11
  "src/",
12
+ "build.sh",
12
13
  "skills/",
13
14
  "README.md"
14
15
  ],
@@ -1,115 +1,40 @@
1
1
  /**
2
- * Build command - Tar up the project and queue a remote build via Sloss agent
2
+ * Build command thin wrapper that execs cli/build.sh with CLI flags passed through.
3
3
  */
4
4
 
5
- import { SlossClient } from '../client.js';
6
- import { existsSync, readFileSync } from 'fs';
7
- import { resolve, basename } from 'path';
8
- import { execSync } from 'child_process';
9
- import { tmpdir } from 'os';
10
- import { join } from 'path';
11
- import { formatBuild } from '../format.js';
5
+ import { resolve, dirname, join } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { execFileSync } from 'child_process';
12
8
 
13
- export async function buildCommand(options, config) {
14
- const platform = (options.platform || 'ios').toLowerCase();
15
- const profile = (options.profile || 'development').toLowerCase();
16
- const bump = options.bump || 'patch';
17
- const projectDir = resolve(options.dir || '.');
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const BUILD_SCRIPT = resolve(__dirname, '../../build.sh');
18
11
 
19
- // Validate platform
20
- if (!['ios', 'android'].includes(platform)) {
21
- throw new Error('Platform must be "ios" or "android"');
22
- }
12
+ export async function buildCommand(options) {
13
+ const args = [];
23
14
 
24
- // Validate profile
25
- if (!['development', 'preview', 'production'].includes(profile)) {
26
- throw new Error('Profile must be "development", "preview", or "production"');
15
+ if (options.platform) {
16
+ args.push('--platform', options.platform);
27
17
  }
28
-
29
- // Check for .sloss.json
30
- const slossConfigPath = join(projectDir, '.sloss.json');
31
- if (!existsSync(slossConfigPath)) {
32
- throw new Error(
33
- `.sloss.json not found in ${projectDir}\n` +
34
- ' Create a .sloss.json config file in your project root.\n' +
35
- ' See: https://github.com/aualdrich/sloss#build-agent'
36
- );
18
+ if (options.profile) {
19
+ args.push('--profile', options.profile);
37
20
  }
38
-
39
- // Read config for display
40
- const slossConfig = JSON.parse(readFileSync(slossConfigPath, 'utf8'));
41
-
42
- console.log('╔══════════════════════════════════════════╗');
43
- console.log('║ SLOSS BUILD ║');
44
- console.log('╠══════════════════════════════════════════╣');
45
- console.log(`║ app : ${(slossConfig.app_name || '—').padEnd(27)} ║`);
46
- console.log(`║ platform : ${platform.padEnd(27)} ║`);
47
- console.log(`║ profile : ${profile.padEnd(27)} ║`);
48
- console.log(`║ bump : ${bump.padEnd(27)} ║`);
49
- console.log(`║ server : ${config.baseUrl.padEnd(27)} ║`);
50
- console.log('╚══════════════════════════════════════════╝');
51
- console.log('');
52
-
53
- // Create tarball
54
- console.log('📦 Packaging project...');
55
- const tarballPath = join(tmpdir(), `sloss-build-${Date.now()}.tar.gz`);
56
-
57
- // Use git archive if in a git repo (respects .gitignore), otherwise tar with excludes
58
- let tarCmd;
59
- try {
60
- execSync('git rev-parse --git-dir', { cwd: projectDir, stdio: 'pipe' });
61
- // git archive from the repo root, only including the project subdir if needed
62
- const gitRoot = execSync('git rev-parse --show-toplevel', { cwd: projectDir, encoding: 'utf8' }).trim();
63
- const relPath = projectDir.replace(gitRoot, '').replace(/^\//, '');
64
-
65
- if (relPath) {
66
- // Project is in a subdirectory — include only that dir
67
- tarCmd = `cd "${gitRoot}" && git archive --format=tar HEAD -- "${relPath}" | gzip > "${tarballPath}"`;
68
- } else {
69
- tarCmd = `cd "${projectDir}" && git archive --format=tar.gz HEAD > "${tarballPath}"`;
70
- }
71
- } catch {
72
- // Not a git repo — fall back to tar with common excludes
73
- tarCmd = `cd "${projectDir}" && tar czf "${tarballPath}" --exclude=node_modules --exclude=.git --exclude=ios/build --exclude=android/build .`;
21
+ if (options.bump) {
22
+ args.push('--bump', options.bump);
74
23
  }
75
-
76
- execSync(tarCmd, { stdio: 'pipe' });
77
-
78
- // Get tarball size for display
79
- const { statSync } = await import('fs');
80
- const tarSize = statSync(tarballPath).size;
81
- const sizeMB = (tarSize / (1024 * 1024)).toFixed(1);
82
- console.log(` → ${sizeMB} MB`);
83
-
84
- // Read version info from .sloss.json's version_file
85
- let version = '';
86
- let buildNumber = '';
87
- if (slossConfig.version_file) {
88
- const versionFilePath = join(projectDir, slossConfig.version_file);
89
- if (existsSync(versionFilePath)) {
90
- const versionData = JSON.parse(readFileSync(versionFilePath, 'utf8'));
91
- version = versionData.version || '';
92
- buildNumber = versionData.buildNumber || '';
93
- }
24
+ if (options.dir) {
25
+ args.push('--dir', resolve(options.dir));
26
+ }
27
+ if (options.yes) {
28
+ args.push('--yes');
94
29
  }
95
30
 
96
- // Upload tarball to start build
97
- console.log('🚀 Queuing build...');
98
- const client = new SlossClient(config.baseUrl, config.apiKey);
99
- const result = await client.startBuild(tarballPath, {
100
- profile,
101
- platform,
102
- bump,
103
- appName: slossConfig.app_name || '',
104
- bundleId: slossConfig.bundle_id || '',
105
- version,
106
- buildNumber,
107
- });
108
-
109
- // Clean up tarball
110
- const { unlinkSync } = await import('fs');
111
- try { unlinkSync(tarballPath); } catch { /* ignore */ }
112
-
113
- console.log('');
114
- console.log(formatBuild(result, config.jsonMode));
31
+ try {
32
+ execFileSync('bash', [BUILD_SCRIPT, ...args], {
33
+ stdio: 'inherit',
34
+ env: process.env,
35
+ });
36
+ } catch (error) {
37
+ // build.sh handles its own error messages; just exit with the same code
38
+ process.exit(error.status || 1);
39
+ }
115
40
  }