@sinch/cli 0.4.17 → 0.5.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
@@ -14,15 +14,26 @@ The Sinch CLI is your unified developer tool for managing all Sinch products. Cu
14
14
  ```bash
15
15
  # Install
16
16
  npm install -g @sinch/cli
17
-
18
- # Dev channel (bleeding edge - latest from main branch)
19
- npm install -g @sinch/cli@dev
20
17
  ```
21
18
 
22
19
  **Installation Channels:**
23
20
 
24
- - **`@latest`** (default) - Production-ready stable releases
25
- - **`@dev`** - Cutting-edge builds from main branch (auto-published on every commit)
21
+ - **`@latest`** (default) - Production-ready stable releases published to npmjs
22
+ - **`@dev`** - Cutting-edge builds auto-published to the [GitLab project registry](https://gitlab.com/sinch/sinch-projects/voice/functions/sinch-cli/-/packages) on every push
23
+
24
+ **How it works — per-architecture packages:**
25
+
26
+ `@sinch/cli` is a thin launcher. The actual binary ships in a platform-specific package that npm selects automatically at install time:
27
+
28
+ | Package | Platform |
29
+ | ------------------------- | ------------------------------- |
30
+ | `@sinch/cli-linux-x64` | Linux x64 |
31
+ | `@sinch/cli-linux-arm64` | Linux ARM64 |
32
+ | `@sinch/cli-darwin-x64` | macOS Intel |
33
+ | `@sinch/cli-darwin-arm64` | macOS Apple Silicon |
34
+ | `@sinch/cli-win32-x64` | Windows x64 / ARM64 (emulation) |
35
+
36
+ This is the same model used by esbuild and Biome. No install scripts run — `npm install --ignore-scripts` works fine. Do **not** use `--omit=optional`; the platform binary is an optional dependency and the CLI will print an actionable error if it is missing.
26
37
 
27
38
  ## Prerequisites
28
39
 
@@ -216,9 +227,26 @@ DEBUG=3 sinch functions dev # Full debug with headers
216
227
  ### Common Issues
217
228
 
218
229
  - **Command not found**: Reinstall globally with `npm install -g @sinch/cli`
230
+ - **"platform package not installed" error**: Your package manager skipped optional dependencies. Reinstall without `--omit=optional`: `npm install -g @sinch/cli`
219
231
  - **Authentication failed**: Check your Sinch API credentials with `sinch auth status`
220
232
  - **Deployment failed**: Verify your function configuration and project access
221
233
 
234
+ ## Development
235
+
236
+ See [docs/DEVELOPMENT.md](./docs/DEVELOPMENT.md) for the full contributor guide.
237
+
238
+ **Quick start:**
239
+
240
+ ```bash
241
+ npm install
242
+ npm run build
243
+ npm run setup # copies binary to ~/.sinch/bin/ and installs completions
244
+ ```
245
+
246
+ **Escape hatch — force a specific binary:**
247
+
248
+ `SINCH_CLI_BINARY=/path/to/binary sinch <cmd>` bypasses platform-package resolution and runs the given binary directly. Useful when testing a locally built binary or a downloaded release without installing the npm package.
249
+
222
250
  ## Support
223
251
 
224
252
  - **Documentation**: [Full CLI Documentation](./docs/README.md)
@@ -1,61 +1,88 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
- const PLATFORM_BINARIES = {
5
- 'darwin-arm64': { artifactName: 'sinch-darwin-arm64' },
6
- 'darwin-x64': { artifactName: 'sinch-darwin-x64' },
7
- 'linux-arm64': { artifactName: 'sinch-linux-arm64' },
8
- 'linux-x64': { artifactName: 'sinch-linux-x64' },
9
- 'win32-arm64': { artifactName: 'sinch-windows-x64.exe' },
10
- 'win32-x64': { artifactName: 'sinch-windows-x64.exe' },
4
+ // win32-arm64 deliberately maps to the x64 package: Bun cannot target
5
+ // windows-arm64, and the x64 exe runs under Windows ARM emulation.
6
+ const PLATFORM_PACKAGES = {
7
+ 'darwin-arm64': { packageName: '@sinch/cli-darwin-arm64', binaryName: 'sinch' },
8
+ 'darwin-x64': { packageName: '@sinch/cli-darwin-x64', binaryName: 'sinch' },
9
+ 'linux-arm64': { packageName: '@sinch/cli-linux-arm64', binaryName: 'sinch' },
10
+ 'linux-x64': { packageName: '@sinch/cli-linux-x64', binaryName: 'sinch' },
11
+ 'win32-arm64': { packageName: '@sinch/cli-win32-x64', binaryName: 'sinch.exe' },
12
+ 'win32-x64': { packageName: '@sinch/cli-win32-x64', binaryName: 'sinch.exe' },
11
13
  };
12
14
 
13
15
  function getPlatformKey(platform = process.platform, arch = process.arch) {
14
16
  return `${platform}-${arch}`;
15
17
  }
16
18
 
17
- function getPlatformBinaryInfo(platform = process.platform, arch = process.arch) {
18
- return PLATFORM_BINARIES[getPlatformKey(platform, arch)] ?? null;
19
+ function getPlatformPackageInfo(platform = process.platform, arch = process.arch) {
20
+ return PLATFORM_PACKAGES[getPlatformKey(platform, arch)] ?? null;
19
21
  }
20
22
 
21
- function buildMissingBinaryMessage(platform, arch, binaryPath) {
22
- const binaryHint = binaryPath
23
- ? `Expected binary at ${binaryPath}.`
24
- : 'No binary mapping is defined for this platform.';
25
-
26
- return [
27
- `Sinch CLI does not have an installed binary for ${platform}-${arch}.`,
28
- binaryHint,
29
- 'Common causes: unsupported platform, install scripts were disabled, or the binary download failed.',
30
- 'Try reinstalling with: npm install -g @sinch/cli',
31
- ].join(' ');
23
+ function buildMissingBinaryMessage(platform, arch, packageName) {
24
+ if (!packageName) {
25
+ return (
26
+ `Sinch CLI: no prebuilt binary exists for ${platform}-${arch}. ` +
27
+ 'Supported platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64, win32-arm64.'
28
+ );
29
+ }
30
+ return (
31
+ `Sinch CLI: the platform package ${packageName} for ${platform}-${arch} is not installed. ` +
32
+ 'Common causes: npm ran with --omit=optional, or the package manager skipped optional dependencies. ' +
33
+ 'Fix: reinstall with optional dependencies enabled: npm install -g @sinch/cli'
34
+ );
35
+ }
36
+
37
+ function findPackageJson(packageName, basedir) {
38
+ let dir = basedir;
39
+ for (;;) {
40
+ // path.join handles the '@scope/name' forward slash correctly on all platforms
41
+ const candidate = path.join(dir, 'node_modules', packageName, 'package.json');
42
+ if (fs.existsSync(candidate)) return candidate;
43
+ const parent = path.dirname(dir);
44
+ if (parent === dir) return null;
45
+ dir = parent;
46
+ }
32
47
  }
33
48
 
34
49
  function resolveInstalledBinaryPath(
35
- packageRoot = path.join(__dirname, '..'),
50
+ basedir = __dirname,
36
51
  platform = process.platform,
37
52
  arch = process.arch
38
53
  ) {
39
- const binaryInfo = getPlatformBinaryInfo(platform, arch);
40
- if (!binaryInfo) {
54
+ // Developer/test escape hatch. Validate the override is a real file so a
55
+ // typo'd path fails here with a clear message instead of an opaque spawn ENOENT.
56
+ if (process.env.SINCH_CLI_BINARY) {
57
+ const overridePath = process.env.SINCH_CLI_BINARY;
58
+ if (!fs.statSync(overridePath, { throwIfNoEntry: false })?.isFile()) {
59
+ throw new Error(`SINCH_CLI_BINARY is set to ${overridePath} but no file exists there.`);
60
+ }
61
+ return { binaryPath: overridePath, packageName: null };
62
+ }
63
+
64
+ const info = getPlatformPackageInfo(platform, arch);
65
+ if (!info) {
41
66
  throw new Error(buildMissingBinaryMessage(platform, arch, null));
42
67
  }
43
68
 
44
- const binaryPath = path.join(packageRoot, 'dist', binaryInfo.artifactName);
45
- if (!fs.existsSync(binaryPath)) {
46
- throw new Error(buildMissingBinaryMessage(platform, arch, binaryPath));
69
+ const packageJsonPath = findPackageJson(info.packageName, basedir);
70
+ if (!packageJsonPath) {
71
+ throw new Error(buildMissingBinaryMessage(platform, arch, info.packageName));
72
+ }
73
+
74
+ const binaryPath = path.join(path.dirname(packageJsonPath), 'bin', info.binaryName);
75
+ if (!fs.statSync(binaryPath, { throwIfNoEntry: false })?.isFile()) {
76
+ throw new Error(buildMissingBinaryMessage(platform, arch, info.packageName));
47
77
  }
48
78
 
49
- return {
50
- binaryPath,
51
- artifactName: binaryInfo.artifactName,
52
- };
79
+ return { binaryPath, packageName: info.packageName };
53
80
  }
54
81
 
55
82
  module.exports = {
56
- PLATFORM_BINARIES,
83
+ PLATFORM_PACKAGES,
57
84
  getPlatformKey,
58
- getPlatformBinaryInfo,
85
+ getPlatformPackageInfo,
59
86
  buildMissingBinaryMessage,
60
87
  resolveInstalledBinaryPath,
61
88
  };
package/bin/sinch CHANGED
@@ -18,7 +18,7 @@ function main(argv = process.argv.slice(2)) {
18
18
  });
19
19
 
20
20
  child.on('error', (error) => {
21
- console.error(`Failed to launch ${resolved.artifactName}: ${error.message}`);
21
+ console.error(`Failed to launch ${resolved.binaryPath}: ${error.message}`);
22
22
  process.exit(1);
23
23
  });
24
24
 
package/package.json CHANGED
@@ -1,19 +1,23 @@
1
1
  {
2
2
  "name": "@sinch/cli",
3
- "version": "0.4.17",
3
+ "version": "0.5.0",
4
4
  "description": "Official Sinch CLI - Manage all Sinch products from your terminal",
5
5
  "main": "bin/sinch",
6
6
  "bin": {
7
7
  "sinch": "bin/sinch"
8
8
  },
9
9
  "files": [
10
- "bin/",
11
- "scripts/postinstall.js",
10
+ "bin/sinch",
11
+ "bin/platform-resolver.js",
12
12
  "README.md",
13
13
  "LICENSE"
14
14
  ],
15
- "scripts": {
16
- "postinstall": "node scripts/postinstall.js"
15
+ "optionalDependencies": {
16
+ "@sinch/cli-linux-x64": "0.5.0",
17
+ "@sinch/cli-linux-arm64": "0.5.0",
18
+ "@sinch/cli-darwin-x64": "0.5.0",
19
+ "@sinch/cli-darwin-arm64": "0.5.0",
20
+ "@sinch/cli-win32-x64": "0.5.0"
17
21
  },
18
22
  "author": "Sinch <support@sinch.com> (https://www.sinch.com)",
19
23
  "license": "MIT",
package/bin/credman.exe DELETED
Binary file
@@ -1,341 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * postinstall.js — runs on `npm install -g @sinch/cli`
5
- *
6
- * 1. Downloads the platform binary from Cloudflare R2
7
- * 2. Writes ~/.sinch/completions.json (command tree for PowerShell)
8
- * 3. Installs shell completion into the user's profile (PowerShell, zsh, or bash)
9
- *
10
- * Keep COMPLETION_COMMANDS in sync with src/index.ts.
11
- */
12
-
13
- const fs = require('fs');
14
- const path = require('path');
15
- const os = require('os');
16
- const https = require('https');
17
- const { spawn, spawnSync } = require('child_process');
18
- const { getPlatformBinaryInfo } = require('../bin/platform-resolver');
19
-
20
- const SINCH_DIR = path.join(os.homedir(), '.sinch');
21
- const R2_PUBLIC_URL = 'https://pub-8e148a1cd144409aa2c332e3d724f317.r2.dev';
22
-
23
- const COMPLETION_COMMANDS = {
24
- functions: ['init', 'list', 'deploy', 'download', 'dev', 'status', 'logs', 'delete', 'docs'],
25
- templates: ['list', 'show', 'node', 'csharp', 'python'],
26
- voice: ['applications', 'callouts', 'calls', 'conferences'],
27
- secrets: ['list', 'add', 'get', 'delete', 'clear'],
28
- auth: ['login', 'status', 'logout'],
29
- sip: ['trunks', 'endpoints', 'acls', 'countries', 'credential-lists', 'calls'],
30
- numbers: ['active', 'available', 'regions'],
31
- fax: ['send', 'list', 'get', 'cancel', 'auth-status', 'status'],
32
- conversation: ['send', 'messages', 'contacts', 'conversations', 'apps', 'webhooks'],
33
- skills: ['install', 'list', 'uninstall', 'update'],
34
- porting: ['check', 'config', 'orders', 'documents', 'activation'],
35
- config: ['--set', '--get', '--list'],
36
- completion: ['--shell', '--install'],
37
- };
38
-
39
- const SENTINEL_START = '# ── Sinch CLI completion ──';
40
- const SENTINEL_END = '# ── End Sinch CLI completion ──';
41
-
42
- function getVersion() {
43
- try {
44
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
45
- return pkg.version || '0.0.0';
46
- } catch {
47
- return '0.0.0';
48
- }
49
- }
50
-
51
- function writeCompletionsJson(version) {
52
- const filePath = path.join(SINCH_DIR, 'completions.json');
53
- fs.writeFileSync(filePath, JSON.stringify({ version, commands: COMPLETION_COMMANDS }, null, 2));
54
- }
55
-
56
- function getPowerShellCompletionScript() {
57
- return `${SENTINEL_START}
58
- # PowerShell completion for Sinch CLI
59
- # Reads command tree from ~/.sinch/completions.json (auto-updated by CLI)
60
-
61
- Register-ArgumentCompleter -Native -CommandName sinch -ScriptBlock {
62
- param($wordToComplete, $commandAst, $cursorPosition)
63
-
64
- try {
65
- $jsonPath = Join-Path $env:USERPROFILE '.sinch' 'completions.json'
66
- if (-not (Test-Path $jsonPath)) { return }
67
-
68
- $data = Get-Content $jsonPath -Raw | ConvertFrom-Json
69
- $line = $commandAst.CommandElements
70
- $command = if ($line.Count -gt 1) { $line[1].Value } else { "" }
71
-
72
- if ($line.Count -eq 2) {
73
- $mainCommands = @($data.commands.PSObject.Properties.Name) + @('--version', '--help')
74
- $mainCommands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
75
- [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
76
- }
77
- }
78
- elseif ($line.Count -eq 3 -and $data.commands.$command) {
79
- @($data.commands.$command) | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
80
- [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
81
- }
82
- }
83
- }
84
- catch {
85
- # Silently fail — prevents PowerShell startup errors
86
- }
87
- }
88
- ${SENTINEL_END}`;
89
- }
90
-
91
- // Twin of generateBashCompletionFile in src/index.ts — keep in sync.
92
- function generateBashCompletionFile() {
93
- const mainCmds = Object.keys(COMPLETION_COMMANDS).join(' ') + ' --version --help';
94
- const cases = Object.entries(COMPLETION_COMMANDS)
95
- .map(
96
- ([cmd, subs]) =>
97
- ` ${cmd}) COMPREPLY=( $(compgen -W "${subs.join(' ')}" -- "$cur") ) ;;`
98
- )
99
- .join('\n');
100
-
101
- return `# Bash/Zsh completion for Sinch CLI (auto-generated — do not edit)
102
- _sinch_completion() {
103
- local cur prev
104
- COMPREPLY=()
105
- cur="\${COMP_WORDS[COMP_CWORD]}"
106
- prev="\${COMP_WORDS[COMP_CWORD-1]}"
107
-
108
- case "\${COMP_CWORD}" in
109
- 1)
110
- COMPREPLY=( $(compgen -W "${mainCmds}" -- "$cur") )
111
- ;;
112
- 2)
113
- case "\${prev}" in
114
- ${cases}
115
- esac
116
- ;;
117
- esac
118
- }
119
-
120
- complete -F _sinch_completion sinch
121
- `;
122
- }
123
-
124
- function shellEscapePath(p) {
125
- return "'" + p.replace(/'/g, "'\\''") + "'";
126
- }
127
-
128
- function stripManagedBlocks(content) {
129
- let nextContent = content;
130
- let startIndex = nextContent.indexOf(SENTINEL_START);
131
-
132
- while (startIndex !== -1) {
133
- const lineStart = nextContent.lastIndexOf('\n', startIndex - 1) + 1;
134
- const endIndex = nextContent.indexOf(SENTINEL_END, startIndex);
135
- let removalEnd = endIndex === -1 ? nextContent.length : endIndex + SENTINEL_END.length;
136
-
137
- if (nextContent.slice(removalEnd, removalEnd + 2) === '\r\n') {
138
- removalEnd += 2;
139
- } else if (nextContent[removalEnd] === '\n') {
140
- removalEnd += 1;
141
- }
142
-
143
- nextContent = nextContent.slice(0, lineStart) + nextContent.slice(removalEnd);
144
- startIndex = nextContent.indexOf(SENTINEL_START);
145
- }
146
-
147
- return nextContent.replace(/\n{3,}/g, '\n\n').trimEnd();
148
- }
149
-
150
- function upsertBlock(content, block) {
151
- const normalizedBlock = block.trim();
152
- const strippedContent = stripManagedBlocks(content);
153
-
154
- if (!strippedContent) {
155
- return normalizedBlock + '\n';
156
- }
157
-
158
- return `${strippedContent}\n\n${normalizedBlock}\n`;
159
- }
160
-
161
- function tryShell(cmd) {
162
- return new Promise((resolve) => {
163
- const ps = spawn(cmd, ['-NoProfile', '-Command', '$PROFILE'], {
164
- stdio: ['ignore', 'pipe', 'pipe'],
165
- });
166
- let out = '';
167
- ps.stdout.on('data', (d) => (out += d.toString()));
168
- ps.on('close', (code) => resolve(code === 0 ? out.trim() : null));
169
- ps.on('error', () => resolve(null));
170
- });
171
- }
172
-
173
- async function installPowerShellCompletion() {
174
- const completionFile = path.join(SINCH_DIR, 'sinch-completion.ps1');
175
- fs.writeFileSync(completionFile, getPowerShellCompletionScript());
176
-
177
- const profilePath = (await tryShell('pwsh')) ?? (await tryShell('powershell'));
178
- if (!profilePath) return;
179
-
180
- const resolved = path.resolve(profilePath);
181
- if (!resolved.startsWith(os.homedir())) return;
182
-
183
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
184
-
185
- let content = '';
186
- try {
187
- content = fs.readFileSync(resolved, 'utf8');
188
- } catch {}
189
-
190
- const sourceLine = `. ${shellEscapePath(completionFile.replace(/\\/g, '\\\\'))}`;
191
- const block = `${SENTINEL_START}\n${sourceLine}\n${SENTINEL_END}`;
192
- fs.writeFileSync(resolved, upsertBlock(content, block));
193
- }
194
-
195
- function installBashZshCompletion() {
196
- const completionFile = path.join(SINCH_DIR, 'sinch-completion.bash');
197
- fs.writeFileSync(completionFile, generateBashCompletionFile());
198
-
199
- const sourceLine = `source ${shellEscapePath(completionFile)}`;
200
- const block = `${SENTINEL_START}\n${sourceLine}\n${SENTINEL_END}`;
201
-
202
- const home = os.homedir();
203
- const zshrc = path.join(home, '.zshrc');
204
- const bashrc = path.join(home, '.bashrc');
205
-
206
- const rcFiles = [];
207
- if (process.platform === 'darwin') {
208
- rcFiles.push(zshrc);
209
- if (fs.existsSync(bashrc)) rcFiles.push(bashrc);
210
- } else {
211
- if (fs.existsSync(bashrc)) rcFiles.push(bashrc);
212
- if (fs.existsSync(zshrc)) rcFiles.push(zshrc);
213
- if (rcFiles.length === 0) rcFiles.push(bashrc);
214
- }
215
-
216
- for (const rcFile of rcFiles) {
217
- let content = '';
218
- try {
219
- content = fs.readFileSync(rcFile, 'utf8');
220
- } catch {}
221
- fs.writeFileSync(rcFile, upsertBlock(content, block));
222
- }
223
- }
224
-
225
- function downloadFile(url, dest) {
226
- return new Promise((resolve, reject) => {
227
- const get = (requestUrl) => {
228
- https
229
- .get(requestUrl, (response) => {
230
- if (
231
- response.statusCode >= 300 &&
232
- response.statusCode < 400 &&
233
- response.headers.location
234
- ) {
235
- get(response.headers.location);
236
- return;
237
- }
238
-
239
- if (response.statusCode !== 200) {
240
- reject(new Error(`Download failed: ${response.statusCode} from ${requestUrl}`));
241
- return;
242
- }
243
-
244
- const file = fs.createWriteStream(dest);
245
- response.pipe(file);
246
- file.on('finish', () => {
247
- file.close();
248
- resolve();
249
- });
250
- file.on('error', reject);
251
- })
252
- .on('error', reject);
253
- };
254
-
255
- get(url);
256
- });
257
- }
258
-
259
- async function downloadBinary(version) {
260
- const binaryInfo = getPlatformBinaryInfo();
261
- if (!binaryInfo) {
262
- return;
263
- }
264
-
265
- const pkgRoot = path.join(__dirname, '..');
266
- const distDir = path.join(pkgRoot, 'dist');
267
- const dest = path.join(distDir, binaryInfo.artifactName);
268
- if (fs.existsSync(dest)) {
269
- return;
270
- }
271
-
272
- fs.mkdirSync(distDir, { recursive: true });
273
- const url = `${R2_PUBLIC_URL}/v${version}/${binaryInfo.artifactName}`;
274
-
275
- try {
276
- await downloadFile(url, dest);
277
- if (!dest.endsWith('.exe')) {
278
- fs.chmodSync(dest, 0o755);
279
- }
280
- } catch (error) {
281
- try {
282
- fs.rmSync(dest, { force: true });
283
- } catch {}
284
- console.error(`Failed to download Sinch CLI binary: ${error.message}`);
285
- console.error(`Source: ${url}`);
286
- }
287
- }
288
-
289
- // Detect-only — never install. We deliberately do NOT shell out to apt/dnf/etc
290
- // here: needs sudo, distro-specific, and may trigger corporate EDR. The user
291
- // gets a one-shot hint at install time; from there it's their call.
292
- function warnIfLinuxKeychainMissing() {
293
- if (process.platform !== 'linux') return;
294
- try {
295
- const probe = spawnSync('secret-tool', ['--version'], { stdio: 'ignore' });
296
- if (probe.status === 0) return;
297
- } catch {
298
- // ENOENT → fall through to warning
299
- }
300
- process.stderr.write(
301
- '\n ⚠️ libsecret-tools is not installed. Sinch CLI needs `secret-tool` to store\n' +
302
- ' credentials securely in the OS keyring on Linux. Without it, secrets will\n' +
303
- ' be written to plaintext config (~/.sinch/profiles/<active>.json).\n\n' +
304
- ' Debian/Ubuntu: sudo apt install libsecret-tools gnome-keyring dbus-x11\n' +
305
- ' Fedora/RHEL: sudo dnf install libsecret gnome-keyring dbus-tools\n' +
306
- ' Arch: sudo pacman -S libsecret gnome-keyring dbus\n\n'
307
- );
308
- }
309
-
310
- async function main() {
311
- if (process.env.npm_config_global !== 'true' && !process.env.SINCH_FORCE_POSTINSTALL) {
312
- return;
313
- }
314
-
315
- try {
316
- fs.mkdirSync(SINCH_DIR, { recursive: true });
317
-
318
- const version = getVersion();
319
- await downloadBinary(version);
320
- writeCompletionsJson(version);
321
-
322
- if (process.platform === 'win32') {
323
- await installPowerShellCompletion();
324
- } else {
325
- installBashZshCompletion();
326
- }
327
-
328
- warnIfLinuxKeychainMissing();
329
- } catch {}
330
- }
331
-
332
- if (require.main === module) {
333
- main();
334
- }
335
-
336
- module.exports = {
337
- stripManagedBlocks,
338
- upsertBlock,
339
- installBashZshCompletion,
340
- generateBashCompletionFile,
341
- };