@tealstreet/cli 0.9.3

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 ADDED
@@ -0,0 +1,271 @@
1
+ # Tealstreet CLI
2
+
3
+ Trade crypto from your terminal. Built on **Bun**, compiled to standalone
4
+ cross-platform binaries (darwin-arm64/x64, linux-x64/arm64).
5
+
6
+ > **Looking for usage docs?** Commands, account setup, sizing/price grammar,
7
+ > workflow operators, and recipes live at **<https://docs.tealstreet.io/cli/>**
8
+ > (source in [`apps/documentation/cli/`](../documentation/cli/)). **This README
9
+ > is for working on the CLI itself** — see that site for anything user-facing.
10
+
11
+ While the REPL is running it also acts as a loopback side-car for the web app
12
+ on the same machine — see
13
+ [`operations/listener.md`](../documentation/cli/operations/listener.md) for the
14
+ listener wire protocol, auth, and kill switches.
15
+
16
+ ## Runtime
17
+
18
+ **Bun**, not Node.js (`bun-types` in tsconfig). Binaries are produced with
19
+ `bun build --compile`. Exchange integration delegates to
20
+ [`@tealstreet/safe-cex`](../../packages/safe-cex); shared command logic lives in
21
+ [`@tealstreet/cli-core`](../../packages/cli-core).
22
+
23
+ ## Develop locally
24
+
25
+ Prerequisite: [Bun](https://bun.sh/) — the CLI's scripts invoke `bun`
26
+ directly. Install workspace deps from the **monorepo root** first (this is a
27
+ yarn workspace; `@tealstreet/*` deps resolve from there).
28
+
29
+ ```bash
30
+ yarn install # from the monorepo root
31
+
32
+ cd apps/cli
33
+ yarn dev # run the REPL from source (= bun run src/index.ts)
34
+ yarn typecheck # tsc --noEmit
35
+ yarn test # vitest
36
+ yarn logs # tail ~/.tealstreet/cli.log
37
+ ```
38
+
39
+ ### Build a local binary
40
+
41
+ `yarn build` compiles standalone binaries for all four platforms (via
42
+ `bun build --compile`) into `dist/tealstreet-<platform>`. To build and run the
43
+ one for your machine:
44
+
45
+ ```bash
46
+ yarn build
47
+ ./dist/tealstreet-darwin-arm64 # or -darwin-x64 / -linux-x64 / -linux-arm64
48
+ ```
49
+
50
+ ### npm package
51
+
52
+ The public `@tealstreet/cli` package is a thin installer/launcher. It does not
53
+ ship CLI source; it downloads the matching binary from
54
+ `https://github.com/Tealstreet/cli/releases` on install or first run.
55
+
56
+ Config and runtime state live under `~/.tealstreet/` — see
57
+ [`operations/config-files.md`](../documentation/cli/operations/config-files.md).
58
+
59
+ ## Architecture
60
+
61
+ [`CLAUDE.md`](./CLAUDE.md) has the full source map, the listener subsystem, the
62
+ remote-signing model, the logging taxonomy, and the script-types pipeline.
63
+ High-level entry points:
64
+
65
+ - `src/index.ts` — Commander.js entry
66
+ - `src/repl.ts` — interactive REPL (readline)
67
+ - `src/listener/` — loopback `127.0.0.1:53219` WS + HTTP side-car for the web app
68
+ - `src/commands/` — command implementations (shared logic in `@tealstreet/cli-core`)
69
+ - `src/config.ts` — `~/.tealstreet/config.json` (accounts, listener kill-switches, footer layout)
70
+ - `src/logger.ts` + `src/log-classify.ts` — console interception, severity taxonomy, operational-noise routing
71
+ - `src/scroll-region.ts` + `src/status-line.ts` + `src/footer.ts` + `src/footer-segments.ts` — the pinned output surfaces (top alert bar + bottom status footer over one shared ANSI scroll region)
72
+ - `src/audit-log.ts` — durable append-only write-op trail + command tracing (`audit` command)
73
+
74
+ ## Development
75
+
76
+ ### Watch Mode Pattern (Terminal Takeover)
77
+
78
+ Watch modes (wa, wp, wo, wm, watch chart, watch chase) need to "take over" the terminal to:
79
+
80
+ 1. Display live-updating content
81
+ 2. Capture single keystrokes to exit (Escape or Ctrl+X in REPL mode, also Ctrl+C when running via CLI args)
82
+ 3. Prevent the REPL prompt from showing
83
+
84
+ **Exit Keys** (centralized in `utils/exit-keys.ts`):
85
+
86
+ - **REPL mode**: Escape or Ctrl+X (Ctrl+C stays with readline for line cancel)
87
+ - **CLI args mode**: Ctrl+C, Escape, or Ctrl+X
88
+
89
+ **The Pattern** (see `startWatchAccount` in repl.ts):
90
+
91
+ ```typescript
92
+ import { isReplExitKey, getReplExitMessage } from './utils/exit-keys';
93
+
94
+ function startWatchMode(rl: readline.Interface): void {
95
+ // 1. Set isMonitorMode flag - CRITICAL! This prevents REPL prompt from showing
96
+ isMonitorMode = true;
97
+
98
+ // 2. Pause readline to stop it from processing input
99
+ rl.pause();
100
+
101
+ // 3. Enter alternate screen buffer (optional - keeps scrollback clean)
102
+ process.stdout.write(ENTER_ALT_SCREEN);
103
+
104
+ // 4. Set up raw mode key handler for exit keys
105
+ const onRawKey = (data: Buffer) => {
106
+ const key = data[0];
107
+ if (isReplExitKey(key)) {
108
+ stopWatch();
109
+ }
110
+ };
111
+
112
+ const stopWatch = () => {
113
+ if (!isMonitorMode) return; // Prevent double-stop
114
+ isMonitorMode = false;
115
+
116
+ // Clean up: remove key handler, exit raw mode, exit alt screen
117
+ process.stdin.removeListener('data', onRawKey);
118
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
119
+ process.stdout.write(EXIT_ALT_SCREEN);
120
+
121
+ // CRITICAL: Recreate readline to restore terminal state
122
+ recreateReadline();
123
+ };
124
+
125
+ // 5. Store stop function for cleanup (used by readline close handler)
126
+ stopMonitorFn = stopWatch;
127
+
128
+ // 6. Enter raw mode and start listening
129
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
130
+ process.stdin.on('data', onRawKey);
131
+
132
+ // 7. Return immediately! The function does NOT block.
133
+ // Watch mode runs via setInterval
134
+ }
135
+ ```
136
+
137
+ **Why isMonitorMode is CRITICAL**:
138
+
139
+ - The REPL's command handler always shows the prompt after a command completes
140
+ - `isMonitorMode = true` suppresses this behavior (checked before calling `rl.prompt()`)
141
+ - Without this flag, the REPL prompt appears immediately after the watch command "returns"
142
+
143
+ **Key Points**:
144
+
145
+ 1. Watch functions return IMMEDIATELY - they don't await/block
146
+ 2. `isMonitorMode = true` prevents REPL prompt from showing
147
+ 3. Raw mode (`setRawMode(true)`) captures single keystrokes
148
+ 4. Must call `recreateReadline()` on cleanup (raw mode corrupts readline state)
149
+
150
+ ### Raw Mode and Readline Pattern
151
+
152
+ Watch modes use raw stdin mode (`setRawMode(true)`) to capture single keystrokes (Escape, Ctrl+X). This corrupts Node.js readline's internal state, causing issues like:
153
+
154
+ - Double-printed characters
155
+ - Arrow keys printing escape sequences (`[A`) instead of navigating history
156
+ - Random input clearing
157
+
158
+ **Solution**: After exiting raw mode, **recreate the readline interface entirely** instead of just resuming it. The `recreateReadline()` helper in `repl.ts` handles this:
159
+
160
+ ```typescript
161
+ function recreateReadline(): void {
162
+ if (!rlInstance) return;
163
+
164
+ // Preserve history
165
+ const history = (rlInstance as readline.Interface & { history?: string[] }).history || [];
166
+
167
+ // Close old readline (with flag to prevent exit handler)
168
+ isRecreatingReadline = true;
169
+ rlInstance.close();
170
+ isRecreatingReadline = false;
171
+
172
+ // Create fresh readline
173
+ rlInstance = readline.createInterface({
174
+ input: process.stdin,
175
+ output: process.stdout,
176
+ prompt: getPrompt(),
177
+ history: [...history],
178
+ historySize: MAX_HISTORY_SIZE,
179
+ });
180
+
181
+ // Re-setup event handlers...
182
+ }
183
+ ```
184
+
185
+ **Important**: All watch mode stop functions must call `recreateReadline()` to restore proper terminal state.
186
+
187
+ ### Console output, logging & status surfaces
188
+
189
+ `src/logger.ts` intercepts `console.*` and routes each line through, in order:
190
+ **operational-noise suppression** → **rate limit** → **readline-aware
191
+ printing**. The full taxonomy and the user-facing behaviour are documented in
192
+ [`CLAUDE.md`](./CLAUDE.md#logging) and the public
193
+ [Output reference](../documentation/cli/reference/output.md) — keep all three in
194
+ sync (see the doc-sync table at the top of `CLAUDE.md`). The essentials:
195
+
196
+ - **Operational noise → file only.** Lines matching `OPERATIONAL_PATTERNS` in
197
+ `src/log-classify.ts` (keepalive pings, reconnect chatter, balance dumps) are
198
+ kept out of the terminal — they still hit `~/.tealstreet/cli.log`. Add new
199
+ noisy prefixes there, not in `logger.ts`.
200
+ - **Everything else prints _above_ the prompt** via `writeAbovePrompt`
201
+ (readline-aware), so async output never corrupts what the user is typing.
202
+ `console.warn` / `console.error` (and any line `log-classify.ts` rates
203
+ `critical`) are additionally mirrored on the **top status bar**
204
+ (`src/status-line.ts`). ("critical" is a severity classification, not a
205
+ `console` method.)
206
+ - **`print()` / `originalConsoleLog()` bypass all of the above** — use them for
207
+ primary command output, prompts, progress, and anything the user explicitly
208
+ asked for.
209
+ - **Verbose transport diagnostics share `cli.log`.** When the CLI runs with
210
+ `DEBUG=1` or `DEBUG=true`, tunneled HoW/tradetunnel REST requests emit
211
+ sanitized `[TradeTunnel]` request/response summaries through the existing
212
+ `console.log` interception. The `[TradeTunnel]` prefix is classified as
213
+ operational, so those lines land in `~/.tealstreet/cli.log` and stay off the
214
+ terminal. This is off by default; it is not a separate network log file.
215
+
216
+ ```typescript
217
+ import { originalConsoleLog } from '../logger';
218
+ const print = originalConsoleLog; // never throttled / suppressed
219
+ print(chalk.green('✓ Operation complete'));
220
+ ```
221
+
222
+ **Pinned surfaces** (one shared ANSI scroll region, `src/scroll-region.ts`):
223
+ the transient **top alert bar** (`status-line.ts`, warn/error/critical, dims)
224
+ and the always-on **bottom footer** (`footer.ts` + `footer-segments.ts`,
225
+ user-customizable segments). Both no-op on non-TTY.
226
+
227
+ **Command tracing**: write-ops can be recorded to a durable append-only
228
+ `~/.tealstreet/audit.log` with per-invocation `runId` + fill attribution
229
+ (`src/audit-log.ts`, `audit` command).
230
+
231
+ ## Building & Releasing
232
+
233
+ ### Prerequisites
234
+
235
+ - [Bun](https://bun.sh/) - `curl -fsSL https://bun.sh/install | bash`
236
+
237
+ ### Build (local only)
238
+
239
+ ```bash
240
+ # Build binaries for all platforms (darwin-arm64, darwin-x64, linux-x64, linux-arm64)
241
+ bun run build
242
+ ```
243
+
244
+ Binaries are output to `dist/`.
245
+
246
+ ### Release to GitHub
247
+
248
+ ```bash
249
+ # Release with version bump
250
+ yarn release:patch # 0.1.0 -> 0.1.1
251
+ yarn release:minor # 0.1.0 -> 0.2.0
252
+ yarn release:major # 0.1.0 -> 1.0.0
253
+
254
+ # Release current package.json version
255
+ yarn release
256
+ ```
257
+
258
+ This will:
259
+
260
+ 1. Bump version in package.json (if specified)
261
+ 2. Commit the version bump
262
+ 3. Push a private monorepo tag named `cli-vX.Y.Z`
263
+ 4. Let GitHub Actions build binaries, publish public `Tealstreet/cli`
264
+ release `vX.Y.Z`, and update `Tealstreet/homebrew-tap`
265
+
266
+ The workflow uses repository secret `TEALSTREET_CLI_RELEASE_TOKEN` to write
267
+ to the public release repo and Homebrew tap.
268
+
269
+ ## License
270
+
271
+ MIT
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawnSync } = require('node:child_process');
5
+ const { ensureBinary } = require('../scripts/install-binary.cjs');
6
+
7
+ const binary = ensureBinary({ optional: false });
8
+ const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
9
+
10
+ if (result.error) {
11
+ console.error(result.error.message);
12
+ process.exit(1);
13
+ }
14
+
15
+ if (result.signal) {
16
+ process.kill(process.pid, result.signal);
17
+ }
18
+
19
+ process.exit(result.status ?? 0);
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@tealstreet/cli",
3
+ "version": "0.9.3",
4
+ "description": "Tealstreet CLI - Trade crypto from your terminal",
5
+ "main": "bin/tealstreet.cjs",
6
+ "type": "module",
7
+ "bin": {
8
+ "tealstreet": "bin/tealstreet.cjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "scripts/install-binary.cjs",
13
+ "README.md"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Tealstreet/tealstreet-next.git",
18
+ "directory": "apps/cli"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "scripts": {
27
+ "cli": "bun src/index.ts",
28
+ "dev": "bun run src/index.ts",
29
+ "build": "./scripts/build.sh",
30
+ "release": "./scripts/release.sh",
31
+ "release:patch": "./scripts/release.sh patch",
32
+ "release:minor": "./scripts/release.sh minor",
33
+ "release:major": "./scripts/release.sh major",
34
+ "lint": "eslint src",
35
+ "version:patch": "yarn version --patch --no-git-tag-version",
36
+ "version:minor": "yarn version --minor --no-git-tag-version",
37
+ "version:major": "yarn version --major --no-git-tag-version",
38
+ "typecheck": "tsc --noEmit --skipLibCheck",
39
+ "logs": "tail -f ~/.tealstreet/cli.log",
40
+ "test": "vitest",
41
+ "test-ci": "vitest run",
42
+ "postinstall": "node scripts/install-binary.cjs --optional"
43
+ },
44
+ "dependencies": {
45
+ "@tealstreet/cli-core": "*",
46
+ "@tealstreet/safe-cex": "*",
47
+ "@tealstreet/script-api-cli": "*",
48
+ "@tealstreet/script-api-core": "*",
49
+ "@tealstreet/script-api-web": "*",
50
+ "@tealstreet/utils": "*",
51
+ "chalk": "^5.3.0",
52
+ "commander": "^15.0.0",
53
+ "fast-json-patch": "^3.1.1",
54
+ "open": "^11.0.0",
55
+ "ora": "^9.4.0",
56
+ "tcp-websocket": "^1.0.0",
57
+ "ws": "^8.21.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^20.0.0",
61
+ "@types/ws": "^8.18.1",
62
+ "bun-types": "^1.3.14",
63
+ "typescript": "^6.0.3",
64
+ "vite-tsconfig-paths": "^5.1.4",
65
+ "vitest": "^4.1.7"
66
+ }
67
+ }
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const crypto = require('node:crypto');
6
+ const https = require('node:https');
7
+ const os = require('node:os');
8
+ const path = require('node:path');
9
+
10
+ const packageJson = require('../package.json');
11
+
12
+ const MAX_REDIRECTS = 10;
13
+ const REQUEST_TIMEOUT_MS = 60_000;
14
+
15
+ function resolvePlatform() {
16
+ const platformMap = {
17
+ darwin: 'darwin',
18
+ linux: 'linux',
19
+ };
20
+ const archMap = {
21
+ arm64: 'arm64',
22
+ x64: 'x64',
23
+ };
24
+
25
+ const platform = platformMap[process.platform];
26
+ const arch = archMap[process.arch];
27
+
28
+ if (!platform || !arch) {
29
+ throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
30
+ }
31
+
32
+ return `${platform}-${arch}`;
33
+ }
34
+
35
+ function getCacheDir() {
36
+ const root =
37
+ process.env.TEALSTREET_CLI_CACHE_DIR ||
38
+ process.env.XDG_CACHE_HOME ||
39
+ path.join(os.homedir(), '.cache');
40
+ return path.join(root, 'tealstreet-cli', packageJson.version);
41
+ }
42
+
43
+ function getBinaryPath() {
44
+ const target = resolvePlatform();
45
+ return path.join(getCacheDir(), `tealstreet-${target}`);
46
+ }
47
+
48
+ function sha256File(file) {
49
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
50
+ }
51
+
52
+ function validateDownloadInputs(url, destination) {
53
+ if (!url || !destination) {
54
+ throw new Error(
55
+ '--download-only requires TEALSTREET_CLI_DOWNLOAD_URL and TEALSTREET_CLI_DOWNLOAD_DESTINATION',
56
+ );
57
+ }
58
+
59
+ const parsedUrl = new URL(url);
60
+ if (parsedUrl.protocol !== 'https:') {
61
+ throw new Error(`Unsupported download URL protocol: ${parsedUrl.protocol}`);
62
+ }
63
+
64
+ if (!path.isAbsolute(destination)) {
65
+ throw new Error('TEALSTREET_CLI_DOWNLOAD_DESTINATION must be an absolute path');
66
+ }
67
+ }
68
+
69
+ function requestUrl(url, onResponse, redirectDepth = 0) {
70
+ return new Promise((resolve, reject) => {
71
+ if (redirectDepth > MAX_REDIRECTS) {
72
+ reject(new Error(`Too many redirects for ${url}`));
73
+ return;
74
+ }
75
+
76
+ const req = https.get(url, (response) => {
77
+ if (
78
+ response.statusCode >= 300 &&
79
+ response.statusCode < 400 &&
80
+ response.headers.location
81
+ ) {
82
+ const nextUrl = new URL(response.headers.location, url).toString();
83
+ response.resume();
84
+ requestUrl(nextUrl, onResponse, redirectDepth + 1).then(resolve, reject);
85
+ return;
86
+ }
87
+
88
+ try {
89
+ onResponse(response, resolve, reject);
90
+ } catch (error) {
91
+ reject(error);
92
+ }
93
+ });
94
+
95
+ req.setTimeout(REQUEST_TIMEOUT_MS, () => {
96
+ req.destroy(new Error(`Request timeout after ${REQUEST_TIMEOUT_MS}ms: ${url}`));
97
+ });
98
+
99
+ req.on('error', (error) => {
100
+ reject(error);
101
+ });
102
+ });
103
+ }
104
+
105
+ function fetchText(url) {
106
+ return requestUrl(url, (response, resolve, reject) => {
107
+ if (response.statusCode !== 200) {
108
+ response.resume();
109
+ reject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`));
110
+ return;
111
+ }
112
+
113
+ response.setEncoding('utf8');
114
+ let body = '';
115
+ response.on('data', (chunk) => {
116
+ body += chunk;
117
+ });
118
+ response.on('end', () => resolve(body));
119
+ response.on('error', reject);
120
+ });
121
+ }
122
+
123
+ function download(url, destination, { expectedSha256 } = {}) {
124
+ return new Promise((resolve, reject) => {
125
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
126
+ const temp = `${destination}.tmp-${process.pid}`;
127
+ let settled = false;
128
+
129
+ const cleanup = () => fs.rmSync(temp, { force: true });
130
+ const fail = (error) => {
131
+ if (settled) return;
132
+ settled = true;
133
+ cleanup();
134
+ reject(error);
135
+ };
136
+ const succeed = () => {
137
+ if (settled) return;
138
+ settled = true;
139
+ resolve();
140
+ };
141
+
142
+ requestUrl(url, (response, requestResolve, requestReject) => {
143
+ if (response.statusCode !== 200) {
144
+ response.resume();
145
+ requestReject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`));
146
+ return;
147
+ }
148
+
149
+ const file = fs.createWriteStream(temp, { mode: 0o755 });
150
+ response.pipe(file);
151
+
152
+ response.on('error', (error) => {
153
+ file.destroy(error);
154
+ });
155
+ file.on('error', requestReject);
156
+ file.on('finish', () => {
157
+ file.close((error) => {
158
+ if (error) {
159
+ requestReject(error);
160
+ return;
161
+ }
162
+
163
+ try {
164
+ if (expectedSha256 && sha256File(temp) !== expectedSha256) {
165
+ requestReject(new Error(`Checksum mismatch for ${url}`));
166
+ return;
167
+ }
168
+
169
+ fs.chmodSync(temp, 0o755);
170
+ fs.renameSync(temp, destination);
171
+ requestResolve();
172
+ } catch (renameError) {
173
+ requestReject(renameError);
174
+ }
175
+ });
176
+ });
177
+ }).then(succeed, fail);
178
+ });
179
+ }
180
+
181
+ async function fetchExpectedSha256(target) {
182
+ const sumsUrl = `https://github.com/Tealstreet/cli/releases/download/v${packageJson.version}/SHA256SUMS`;
183
+ const sums = await fetchText(sumsUrl);
184
+ for (const line of sums.split(/\r?\n/)) {
185
+ const [hash, file] = line.trim().split(/\s+/);
186
+ if (file === `tealstreet-${target}` && /^[a-f0-9]{64}$/i.test(hash)) {
187
+ return hash;
188
+ }
189
+ }
190
+ throw new Error(`Missing checksum for tealstreet-${target}`);
191
+ }
192
+
193
+ function ensureBinary({ optional = false } = {}) {
194
+ const target = resolvePlatform();
195
+ const binary = getBinaryPath();
196
+
197
+ if (fs.existsSync(binary)) {
198
+ return binary;
199
+ }
200
+
201
+ const url = `https://github.com/Tealstreet/cli/releases/download/v${packageJson.version}/tealstreet-${target}`;
202
+
203
+ try {
204
+ const childProcess = require('node:child_process');
205
+ const expectedSha256 = childProcess
206
+ .execFileSync(process.execPath, [__filename, '--checksum-only'], {
207
+ encoding: 'utf8',
208
+ env: {
209
+ ...process.env,
210
+ TEALSTREET_CLI_CHECKSUM_TARGET: target,
211
+ },
212
+ })
213
+ .trim();
214
+ childProcess.execFileSync(process.execPath, [__filename, '--download-only'], {
215
+ stdio: optional ? 'ignore' : 'inherit',
216
+ env: {
217
+ ...process.env,
218
+ TEALSTREET_CLI_DOWNLOAD_URL: url,
219
+ TEALSTREET_CLI_DOWNLOAD_DESTINATION: binary,
220
+ TEALSTREET_CLI_DOWNLOAD_SHA256: expectedSha256,
221
+ },
222
+ });
223
+ return binary;
224
+ } catch (error) {
225
+ if (optional) {
226
+ console.warn(`tealstreet: skipped binary preinstall (${error.message})`);
227
+ return fs.existsSync(binary) ? binary : null;
228
+ }
229
+ throw error;
230
+ }
231
+ }
232
+
233
+ async function main() {
234
+ const optional = process.argv.includes('--optional');
235
+ const downloadOnly = process.argv.includes('--download-only');
236
+ const checksumOnly = process.argv.includes('--checksum-only');
237
+
238
+ try {
239
+ if (checksumOnly) {
240
+ const target = process.env.TEALSTREET_CLI_CHECKSUM_TARGET;
241
+ if (!target) {
242
+ throw new Error('--checksum-only requires TEALSTREET_CLI_CHECKSUM_TARGET');
243
+ }
244
+ process.stdout.write(await fetchExpectedSha256(target));
245
+ return;
246
+ }
247
+
248
+ if (downloadOnly) {
249
+ const url = process.env.TEALSTREET_CLI_DOWNLOAD_URL;
250
+ const destination = process.env.TEALSTREET_CLI_DOWNLOAD_DESTINATION;
251
+ const expectedSha256 = process.env.TEALSTREET_CLI_DOWNLOAD_SHA256;
252
+ validateDownloadInputs(url, destination);
253
+ await download(url, destination, { expectedSha256 });
254
+ return;
255
+ }
256
+
257
+ ensureBinary({ optional });
258
+ } catch (error) {
259
+ if (optional) {
260
+ console.warn(`tealstreet: skipped binary preinstall (${error.message})`);
261
+ return;
262
+ }
263
+ console.error(error.message);
264
+ process.exit(1);
265
+ }
266
+ }
267
+
268
+ if (require.main === module) {
269
+ main();
270
+ }
271
+
272
+ module.exports = {
273
+ ensureBinary,
274
+ getBinaryPath,
275
+ resolvePlatform,
276
+ };