sloss-cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,14 @@ Command-line interface for [Sloss](https://github.com/aualdrich/sloss) — a sel
8
8
  npm install -g sloss-cli
9
9
  ```
10
10
 
11
+ Or as a project dependency:
12
+
13
+ ```bash
14
+ npm install sloss-cli
15
+ # or
16
+ bun add sloss-cli
17
+ ```
18
+
11
19
  ## Quick Start
12
20
 
13
21
  ```bash
@@ -55,6 +63,33 @@ URL resolution order:
55
63
  --help Show help
56
64
  ```
57
65
 
66
+ ## Publishing to npm
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)
71
+
72
+ ```bash
73
+ # Set your npm token
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
81
+ ```
82
+
83
+ The package is published as [`sloss-cli`](https://www.npmjs.com/package/sloss-cli) under the `front-porch-software` npm account.
84
+
85
+ ### What gets published
86
+
87
+ Only `bin/`, `src/`, and `README.md` are included in the npm package (controlled by `files` in `package.json`). Build scripts and other development files are excluded.
88
+
89
+ ## Related
90
+
91
+ - **[Sloss Server](https://github.com/aualdrich/sloss)** — The distribution server that this CLI talks to
92
+
58
93
  ## License
59
94
 
60
95
  MIT
package/bin/sloss.js CHANGED
@@ -11,6 +11,7 @@ import { listCommand } from '../src/commands/list.js';
11
11
  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
+ import { buildCommand } from '../src/commands/build.js';
14
15
 
15
16
  const program = new Command();
16
17
 
@@ -100,4 +101,22 @@ program
100
101
  }
101
102
  });
102
103
 
104
+ // Build command
105
+ program
106
+ .command('build')
107
+ .description('Queue a build via the Sloss build agent')
108
+ .option('--platform <platform>', 'Platform (ios or android)', 'ios')
109
+ .option('--profile <profile>', 'Build profile (development, preview, production)', 'development')
110
+ .option('--bump <type>', 'Version bump type for production (patch, minor, major)', 'patch')
111
+ .option('--dir <path>', 'Project directory (default: current directory)', '.')
112
+ .action(async (options) => {
113
+ try {
114
+ const config = resolveConfig(program.opts());
115
+ await buildCommand(options, config);
116
+ } catch (error) {
117
+ console.error(`Error: ${error.message}`);
118
+ process.exit(1);
119
+ }
120
+ });
121
+
103
122
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sloss-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "CLI for Sloss — a self-hosted IPA/APK distribution server",
5
5
  "type": "module",
6
6
  "bin": {
package/src/client.js CHANGED
@@ -61,6 +61,33 @@ export class SlossClient {
61
61
  return await this.request('DELETE', `/api/uploads/${id}`);
62
62
  }
63
63
 
64
+ async startBuild(tarballPath, metadata = {}) {
65
+ const fs = await import('fs/promises');
66
+ const path = await import('path');
67
+
68
+ const fileBuffer = await fs.readFile(tarballPath);
69
+ const fileName = path.basename(tarballPath);
70
+
71
+ const formData = new FormData();
72
+ const blob = new Blob([fileBuffer]);
73
+ formData.append('tarball', blob, fileName);
74
+
75
+ // Add build metadata
76
+ if (metadata.profile) formData.append('profile', metadata.profile);
77
+ if (metadata.platform) formData.append('platform', metadata.platform);
78
+ if (metadata.bump) formData.append('bump', metadata.bump);
79
+ if (metadata.appName) formData.append('app_name', metadata.appName);
80
+ if (metadata.bundleId) formData.append('bundle_id', metadata.bundleId);
81
+ if (metadata.version) formData.append('version', metadata.version);
82
+ if (metadata.buildNumber) formData.append('build_number', metadata.buildNumber);
83
+
84
+ return await this.request('POST', '/api/builds/start', formData);
85
+ }
86
+
87
+ async getBuild(id) {
88
+ return await this.request('GET', `/api/builds/${id}`);
89
+ }
90
+
64
91
  async uploadFile(filePath, platform, metadata = {}) {
65
92
  const fs = await import('fs/promises');
66
93
  const path = await import('path');
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Build command - Tar up the project and queue a remote build via Sloss agent
3
+ */
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';
12
+
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 || '.');
18
+
19
+ // Validate platform
20
+ if (!['ios', 'android'].includes(platform)) {
21
+ throw new Error('Platform must be "ios" or "android"');
22
+ }
23
+
24
+ // Validate profile
25
+ if (!['development', 'preview', 'production'].includes(profile)) {
26
+ throw new Error('Profile must be "development", "preview", or "production"');
27
+ }
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
+ );
37
+ }
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 .`;
74
+ }
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
+ }
94
+ }
95
+
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));
115
+ }
package/src/format.js CHANGED
@@ -82,6 +82,22 @@ export function formatUpload(result, jsonMode = false) {
82
82
  return lines.join('\n');
83
83
  }
84
84
 
85
+ export function formatBuild(result, jsonMode = false) {
86
+ if (jsonMode) {
87
+ return JSON.stringify(result, null, 2);
88
+ }
89
+
90
+ const lines = [
91
+ '✅ Build queued!',
92
+ '',
93
+ `Build ID: ${result.id}`,
94
+ `Status: ${result.status || 'queued'}`,
95
+ `Page URL: ${result.page_url}`,
96
+ ];
97
+
98
+ return lines.join('\n');
99
+ }
100
+
85
101
  export function formatDelete(jsonMode = false) {
86
102
  if (jsonMode) {
87
103
  return JSON.stringify({ ok: true }, null, 2);