sloss-cli 1.0.0 → 1.2.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
@@ -1,6 +1,6 @@
1
1
  # sloss-cli
2
2
 
3
- Command-line interface for [Sloss](https://github.com/aualdrich/sloss) — a self-hosted IPA/APK distribution server inspired by Diawi.
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
 
@@ -8,38 +8,170 @@ Command-line interface for [Sloss](https://github.com/aualdrich/sloss) — a sel
8
8
  npm install -g sloss-cli
9
9
  ```
10
10
 
11
- ## Quick Start
11
+ Or as a project dependency:
12
+
13
+ ```bash
14
+ npm install --save-dev sloss-cli
15
+ # or
16
+ bun add --dev sloss-cli
17
+ ```
18
+
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
12
26
 
13
27
  ```bash
14
- # Authenticate (saves API key to ~/.config/sloss/credentials.json)
15
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
16
114
 
17
- # List recent builds
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) |
121
+
122
+ ### List builds
123
+
124
+ ```bash
18
125
  sloss list
126
+ sloss list --limit 20
127
+ ```
128
+
129
+ ### Get build details
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):
19
138
 
139
+ ```bash
20
140
  # Upload an IPA
21
- sloss upload path/to/App.ipa --platform ios --profile preview
141
+ sloss upload ./build/MyApp.ipa --platform ios --profile preview
22
142
 
23
143
  # Upload an APK
24
- sloss upload path/to/app.apk --platform android --profile development
144
+ sloss upload ./build/app-release.apk --platform android --profile production
145
+ ```
25
146
 
26
- # Get build details
27
- sloss info <build-id>
147
+ #### Upload options
28
148
 
29
- # Delete a build
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
30
160
  sloss delete <build-id>
31
161
  ```
32
162
 
33
163
  ## Authentication
34
164
 
35
165
  API key resolution order (highest → lowest priority):
166
+
36
167
  1. `--api-key` CLI flag
37
168
  2. `SLOSS_API_KEY` environment variable
38
169
  3. `~/.config/sloss/credentials.json` (saved by `sloss login`)
39
170
 
40
171
  ## Server URL
41
172
 
42
- URL resolution order:
173
+ URL resolution order (highest → lowest priority):
174
+
43
175
  1. `--url` CLI flag
44
176
  2. `SLOSS_URL` environment variable
45
177
  3. `~/.config/sloss/credentials.json`
@@ -47,14 +179,54 @@ URL resolution order:
47
179
 
48
180
  ## Global Options
49
181
 
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
+ }
212
+ ```
213
+
214
+ Or copy the skill into your workspace:
215
+
216
+ ```bash
217
+ cp -r ./node_modules/sloss-cli/skills/sloss <workspace>/skills/sloss
50
218
  ```
51
- --api-key <key> Override API key
52
- --url <url> Override server URL
53
- --json Output as JSON
54
- --version Show version
55
- --help Show help
219
+
220
+ The skill is also available on [ClawHub](https://clawhub.com):
221
+
222
+ ```bash
223
+ clawhub install sloss
56
224
  ```
57
225
 
226
+ ## Related
227
+
228
+ - **[Sloss Server](https://github.com/aualdrich/sloss)** — The self-hosted distribution server
229
+
58
230
  ## License
59
231
 
60
232
  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,22 +1,33 @@
1
1
  {
2
2
  "name": "sloss-cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI for Sloss — a self-hosted IPA/APK distribution server",
5
5
  "type": "module",
6
6
  "bin": {
7
- "sloss": "./bin/sloss.js"
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": ["sloss", "ios", "android", "ipa", "apk", "build", "distribution", "testflight", "diawi"],
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,171 @@
1
+ ---
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)"}]}}
6
+ ---
7
+
8
+ # Sloss — Build Distribution for Expo Apps
9
+
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.
11
+
12
+ ## Prerequisites
13
+
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
17
+
18
+ ## Authentication
19
+
20
+ ```bash
21
+ # Login once — saves API key to ~/.config/sloss/credentials.json
22
+ sloss login
23
+ ```
24
+
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`
34
+
35
+ ## Queue a Build
36
+
37
+ Run from the Expo project root (requires `.sloss.json` config file):
38
+
39
+ ```bash
40
+ # iOS development build (default)
41
+ sloss build
42
+
43
+ # Android preview build
44
+ sloss build --platform android --profile preview
45
+
46
+ # Production build with minor version bump
47
+ sloss build --profile production --bump minor
48
+
49
+ # Build from a different directory
50
+ sloss build --dir /path/to/expo/project
51
+ ```
52
+
53
+ ### Build options
54
+
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 |
61
+
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
72
+
73
+ ```bash
74
+ sloss list
75
+ sloss list --limit 20
76
+ ```
77
+
78
+ ## Build Details
79
+
80
+ ```bash
81
+ sloss info <build-id>
82
+ ```
83
+
84
+ ## Upload a Pre-Built Artifact
85
+
86
+ If you already have an IPA or APK:
87
+
88
+ ```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
94
+ ```
95
+
96
+ Upload options:
97
+
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 |
105
+
106
+ ## Delete a Build
107
+
108
+ ```bash
109
+ sloss delete <build-id>
110
+ ```
111
+
112
+ ## Project Configuration
113
+
114
+ ### `.sloss.json`
115
+
116
+ Place in the Expo project root:
117
+
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
+ }
130
+ ```
131
+
132
+ ### `app.version.json`
133
+
134
+ ```json
135
+ {
136
+ "version": "1.0.0",
137
+ "buildNumber": "1"
138
+ }
139
+ ```
140
+
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
+ ```
153
+
154
+ ## Global Flags
155
+
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 |
163
+
164
+ ## Output Formats
165
+
166
+ All commands support `--json` for structured JSON output:
167
+
168
+ ```bash
169
+ sloss list --json
170
+ sloss info <build-id> --json
171
+ ```
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);