@sinch/cli 0.4.6 → 0.4.8

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.
@@ -0,0 +1,61 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
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' },
11
+ };
12
+
13
+ function getPlatformKey(platform = process.platform, arch = process.arch) {
14
+ return `${platform}-${arch}`;
15
+ }
16
+
17
+ function getPlatformBinaryInfo(platform = process.platform, arch = process.arch) {
18
+ return PLATFORM_BINARIES[getPlatformKey(platform, arch)] ?? null;
19
+ }
20
+
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(' ');
32
+ }
33
+
34
+ function resolveInstalledBinaryPath(
35
+ packageRoot = path.join(__dirname, '..'),
36
+ platform = process.platform,
37
+ arch = process.arch
38
+ ) {
39
+ const binaryInfo = getPlatformBinaryInfo(platform, arch);
40
+ if (!binaryInfo) {
41
+ throw new Error(buildMissingBinaryMessage(platform, arch, null));
42
+ }
43
+
44
+ const binaryPath = path.join(packageRoot, 'dist', binaryInfo.artifactName);
45
+ if (!fs.existsSync(binaryPath)) {
46
+ throw new Error(buildMissingBinaryMessage(platform, arch, binaryPath));
47
+ }
48
+
49
+ return {
50
+ binaryPath,
51
+ artifactName: binaryInfo.artifactName,
52
+ };
53
+ }
54
+
55
+ module.exports = {
56
+ PLATFORM_BINARIES,
57
+ getPlatformKey,
58
+ getPlatformBinaryInfo,
59
+ buildMissingBinaryMessage,
60
+ resolveInstalledBinaryPath,
61
+ };
package/bin/sinch CHANGED
@@ -1,17 +1,40 @@
1
- #!/bin/sh
2
- # Native launcher — execs the Bun-compiled binary, no Node.js required.
3
- # npm registers this via the "bin" field in package.json.
4
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
- ARCH="$(uname -m)"
6
- case "$ARCH" in
7
- x86_64|amd64) ARCH="x64" ;;
8
- aarch64|arm64) ARCH="arm64" ;;
9
- esac
10
- OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
11
- BINARY="$SCRIPT_DIR/../dist/sinch-${OS}-${ARCH}"
12
- if [ ! -f "$BINARY" ]; then
13
- echo "Sinch CLI binary not found: $BINARY" >&2
14
- echo "Reinstall with: npm install -g @sinch/cli" >&2
15
- exit 1
16
- fi
17
- exec "$BINARY" "$@"
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const { resolveInstalledBinaryPath } = require('./platform-resolver');
5
+
6
+ function main(argv = process.argv.slice(2)) {
7
+ let resolved;
8
+
9
+ try {
10
+ resolved = resolveInstalledBinaryPath();
11
+ } catch (error) {
12
+ console.error(error.message);
13
+ process.exit(1);
14
+ }
15
+
16
+ const child = spawn(resolved.binaryPath, argv, {
17
+ stdio: 'inherit',
18
+ });
19
+
20
+ child.on('error', (error) => {
21
+ console.error(`Failed to launch ${resolved.artifactName}: ${error.message}`);
22
+ process.exit(1);
23
+ });
24
+
25
+ child.on('exit', (code, signal) => {
26
+ if (signal) {
27
+ process.kill(process.pid, signal);
28
+ return;
29
+ }
30
+ process.exit(code ?? 0);
31
+ });
32
+ }
33
+
34
+ if (require.main === module) {
35
+ main();
36
+ }
37
+
38
+ module.exports = {
39
+ main,
40
+ };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@sinch/cli",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Official Sinch CLI - Manage all Sinch products from your terminal",
5
+ "main": "bin/sinch",
5
6
  "bin": {
6
- "sinch": "bin/sinch",
7
- "s": "bin/s"
7
+ "sinch": "bin/sinch"
8
8
  },
9
9
  "files": [
10
10
  "bin/",
@@ -3,10 +3,9 @@
3
3
  /**
4
4
  * postinstall.js — runs on `npm install -g @sinch/cli`
5
5
  *
6
- * 1. Writes ~/.sinch/completions.json (command tree for PowerShell)
7
- * 2. Installs shell completion into the user's profile (PowerShell, zsh, or bash)
8
- *
9
- * Silent failure on ALL errors — must never break `npm install`.
6
+ * 1. Downloads the platform binary from GitLab Generic Packages
7
+ * 2. Writes ~/.sinch/completions.json (command tree for PowerShell)
8
+ * 3. Installs shell completion into the user's profile (PowerShell, zsh, or bash)
10
9
  *
11
10
  * Keep COMPLETION_COMMANDS in sync with src/index.ts.
12
11
  */
@@ -15,17 +14,16 @@ const fs = require('fs');
15
14
  const path = require('path');
16
15
  const os = require('os');
17
16
  const https = require('https');
18
- const { spawn } = require('child_process');
17
+ const { execSync, spawn } = require('child_process');
18
+ const { getPlatformBinaryInfo } = require('../bin/platform-resolver');
19
19
 
20
20
  const SINCH_DIR = path.join(os.homedir(), '.sinch');
21
21
  const PROJECT_ID = '72974711';
22
22
 
23
- // Keep in sync with COMPLETION_COMMANDS in src/index.ts.
24
- // The postAction hook overwrites completions.json on every CLI run, so drift self-heals.
25
23
  const COMPLETION_COMMANDS = {
26
24
  functions: ['init', 'list', 'deploy', 'download', 'dev', 'status', 'logs', 'delete', 'docs'],
27
25
  templates: ['list', 'show', 'node', 'csharp', 'python'],
28
- voice: ['callback-url', 'get-callbacks', 'set-callback'],
26
+ voice: ['applications', 'callouts', 'calls', 'conferences'],
29
27
  secrets: ['list', 'add', 'get', 'delete', 'clear'],
30
28
  auth: ['login', 'status', 'logout'],
31
29
  sip: ['trunks', 'endpoints', 'acls', 'countries', 'credential-lists', 'calls'],
@@ -90,20 +88,41 @@ Register-ArgumentCompleter -Native -CommandName sinch -ScriptBlock {
90
88
  ${SENTINEL_END}`;
91
89
  }
92
90
 
93
- // --- Profile installation helpers ---
94
-
95
91
  function shellEscapePath(p) {
96
92
  return "'" + p.replace(/'/g, "'\\''") + "'";
97
93
  }
98
94
 
95
+ function stripManagedBlocks(content) {
96
+ let nextContent = content;
97
+ let startIndex = nextContent.indexOf(SENTINEL_START);
98
+
99
+ while (startIndex !== -1) {
100
+ const lineStart = nextContent.lastIndexOf('\n', startIndex - 1) + 1;
101
+ const endIndex = nextContent.indexOf(SENTINEL_END, startIndex);
102
+ let removalEnd = endIndex === -1 ? nextContent.length : endIndex + SENTINEL_END.length;
103
+
104
+ if (nextContent.slice(removalEnd, removalEnd + 2) === '\r\n') {
105
+ removalEnd += 2;
106
+ } else if (nextContent[removalEnd] === '\n') {
107
+ removalEnd += 1;
108
+ }
109
+
110
+ nextContent = nextContent.slice(0, lineStart) + nextContent.slice(removalEnd);
111
+ startIndex = nextContent.indexOf(SENTINEL_START);
112
+ }
113
+
114
+ return nextContent.replace(/\n{3,}/g, '\n\n').trimEnd();
115
+ }
116
+
99
117
  function upsertBlock(content, block) {
100
- const pattern =
101
- SENTINEL_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
102
- '[\\s\\S]*?' +
103
- SENTINEL_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
104
- const replaced = content.replace(new RegExp(pattern, 'g'), block.trim());
105
- if (replaced !== content) return replaced;
106
- return content + (content && !content.endsWith('\n') ? '\n' : '') + '\n' + block.trim() + '\n';
118
+ const normalizedBlock = block.trim();
119
+ const strippedContent = stripManagedBlocks(content);
120
+
121
+ if (!strippedContent) {
122
+ return normalizedBlock + '\n';
123
+ }
124
+
125
+ return `${strippedContent}\n\n${normalizedBlock}\n`;
107
126
  }
108
127
 
109
128
  function tryShell(cmd) {
@@ -125,7 +144,6 @@ async function installPowerShellCompletion() {
125
144
  const profilePath = (await tryShell('pwsh')) ?? (await tryShell('powershell'));
126
145
  if (!profilePath) return;
127
146
 
128
- // Validate profile path is within user's home directory
129
147
  const resolved = path.resolve(profilePath);
130
148
  if (!resolved.startsWith(os.homedir())) return;
131
149
 
@@ -134,9 +152,7 @@ async function installPowerShellCompletion() {
134
152
  let content = '';
135
153
  try {
136
154
  content = fs.readFileSync(resolved, 'utf8');
137
- } catch {
138
- // Profile doesn't exist yet — will be created
139
- }
155
+ } catch {}
140
156
 
141
157
  const sourceLine = `. ${shellEscapePath(completionFile.replace(/\\/g, '\\\\'))}`;
142
158
  const block = `${SENTINEL_START}\n${sourceLine}\n${SENTINEL_END}`;
@@ -166,57 +182,67 @@ function installBashZshCompletion() {
166
182
  let content = '';
167
183
  try {
168
184
  content = fs.readFileSync(rcFile, 'utf8');
169
- } catch {
170
- // File doesn't exist yet — will be created
171
- }
185
+ } catch {}
172
186
  fs.writeFileSync(rcFile, upsertBlock(content, block));
173
187
  }
174
188
  }
175
189
 
176
- // --- Binary download ---
177
-
178
- const PLATFORM_MAP = {
179
- 'linux-x64': 'sinch-linux-x64',
180
- 'linux-arm64': 'sinch-linux-arm64',
181
- 'darwin-x64': 'sinch-darwin-x64',
182
- 'darwin-arm64': 'sinch-darwin-arm64',
183
- 'win32-x64': 'sinch-windows-x64.exe',
184
- 'win32-arm64': 'sinch-windows-x64.exe',
185
- };
186
-
187
- function getTokenFromNpmrc() {
188
- // Read GitLab token from global .npmrc (same token used for registry auth)
190
+ function getTokenFromNpmConfig() {
189
191
  try {
190
- const { execSync } = require('child_process');
191
- const globalConfig = execSync('npm config get globalconfig', { encoding: 'utf8' }).trim();
192
- const content = fs.readFileSync(globalConfig, 'utf8');
193
- const match = content.match(
194
- new RegExp(`//gitlab\\.com/api/v4/projects/${PROJECT_ID}/packages/npm/:_authToken=(.+)`)
195
- );
196
- return match ? match[1].trim() : '';
192
+ const configPaths = [
193
+ process.env.npm_config_userconfig,
194
+ process.env.NPM_CONFIG_USERCONFIG,
195
+ execSync('npm config get userconfig', { encoding: 'utf8' }).trim(),
196
+ execSync('npm config get globalconfig', { encoding: 'utf8' }).trim(),
197
+ ].filter(Boolean);
198
+
199
+ for (const configPath of configPaths) {
200
+ if (!fs.existsSync(configPath)) {
201
+ continue;
202
+ }
203
+
204
+ const content = fs.readFileSync(configPath, 'utf8');
205
+ const match = content.match(
206
+ new RegExp(`//gitlab\\.com/api/v4/projects/${PROJECT_ID}/packages/npm/:_authToken=(.+)`)
207
+ );
208
+
209
+ if (match?.[1]?.trim()) {
210
+ return match[1].trim();
211
+ }
212
+ }
197
213
  } catch {
198
214
  return '';
199
215
  }
216
+
217
+ return '';
200
218
  }
201
219
 
202
220
  function downloadFile(url, dest, token) {
203
221
  return new Promise((resolve, reject) => {
204
222
  const headers = {};
205
- if (token) headers['PRIVATE-TOKEN'] = token;
223
+ if (token) {
224
+ headers['PRIVATE-TOKEN'] = token;
225
+ }
206
226
 
207
- const get = (reqUrl) => {
227
+ const get = (requestUrl) => {
208
228
  https
209
- .get(reqUrl, { headers }, (res) => {
210
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
211
- get(res.headers.location); // follow redirect
229
+ .get(requestUrl, { headers }, (response) => {
230
+ if (
231
+ response.statusCode >= 300 &&
232
+ response.statusCode < 400 &&
233
+ response.headers.location
234
+ ) {
235
+ get(response.headers.location);
212
236
  return;
213
237
  }
214
- if (res.statusCode !== 200) {
215
- reject(new Error(`Download failed: ${res.statusCode} from ${reqUrl}`));
238
+
239
+ if (response.statusCode !== 200) {
240
+ reject(new Error(`Download failed: ${response.statusCode} from ${requestUrl}`));
216
241
  return;
217
242
  }
243
+
218
244
  const file = fs.createWriteStream(dest);
219
- res.pipe(file);
245
+ response.pipe(file);
220
246
  file.on('finish', () => {
221
247
  file.close();
222
248
  resolve();
@@ -225,89 +251,46 @@ function downloadFile(url, dest, token) {
225
251
  })
226
252
  .on('error', reject);
227
253
  };
254
+
228
255
  get(url);
229
256
  });
230
257
  }
231
258
 
232
259
  async function downloadBinary(version) {
233
- const key = `${process.platform}-${process.arch}`;
234
- const binName = PLATFORM_MAP[key];
235
- if (!binName) return; // unsupported platform — skip silently
260
+ const binaryInfo = getPlatformBinaryInfo();
261
+ if (!binaryInfo) {
262
+ return;
263
+ }
236
264
 
237
265
  const pkgRoot = path.join(__dirname, '..');
238
266
  const distDir = path.join(pkgRoot, 'dist');
239
- const dest = path.join(distDir, binName);
240
-
241
- // Skip if binary already exists (local dev build)
242
- if (fs.existsSync(dest)) return;
267
+ const dest = path.join(distDir, binaryInfo.artifactName);
268
+ if (fs.existsSync(dest)) {
269
+ return;
270
+ }
243
271
 
244
272
  fs.mkdirSync(distDir, { recursive: true });
245
-
246
- const baseUrl = `https://gitlab.com/api/v4/projects/${PROJECT_ID}/packages/generic/sinch-cli/${version}/${binName}`;
247
- const token = process.env.SINCH_GITLAB_TOKEN || process.env.GITLAB_TOKEN || getTokenFromNpmrc();
273
+ const token =
274
+ process.env.SINCH_GITLAB_TOKEN || process.env.GITLAB_TOKEN || getTokenFromNpmConfig();
275
+ const url = `https://gitlab.com/api/v4/projects/${PROJECT_ID}/packages/generic/sinch-cli/${version}/${binaryInfo.artifactName}`;
248
276
 
249
277
  try {
250
- console.log(`Downloading Sinch CLI ${version} for ${key}...`);
251
- await downloadFile(baseUrl, dest, token);
252
-
253
- // Make executable on Unix
254
- if (process.platform !== 'win32') {
278
+ await downloadFile(url, dest, token);
279
+ if (!dest.endsWith('.exe')) {
255
280
  fs.chmodSync(dest, 0o755);
256
281
  }
257
- console.log('Sinch CLI binary downloaded.');
258
- } catch (err) {
259
- // Clean up partial download
282
+ } catch (error) {
260
283
  try {
261
- fs.unlinkSync(dest);
284
+ fs.rmSync(dest, { force: true });
262
285
  } catch {}
263
- console.error(`Failed to download Sinch CLI binary: ${err.message}`);
286
+ console.error(`Failed to download Sinch CLI binary: ${error.message}`);
264
287
  console.error(
265
- `You can download manually from: https://gitlab.com/sinch/sinch-projects/voice/functions/sinch-cli/-/releases`
288
+ 'You can download the binary manually from GitLab release assets or the generic package registry.'
266
289
  );
267
290
  }
268
291
  }
269
292
 
270
- /**
271
- * On Windows, npm generates .cmd and .ps1 shims that try to run bin/sinch via
272
- * /bin/sh (broken on Windows). Replace them with our native .cmd launcher that
273
- * execs the Bun binary directly — zero Node.js overhead.
274
- */
275
- function fixWindowsShims() {
276
- if (process.platform !== 'win32') return;
277
- try {
278
- const prefix = process.env.npm_config_prefix;
279
- if (!prefix || !path.isAbsolute(prefix)) return;
280
-
281
- const globalBinDir = prefix;
282
- const pkgRoot = path.join(__dirname, '..');
283
- const binaryPath = path.join(pkgRoot, 'dist', 'sinch-windows-x64.exe');
284
-
285
- // Write a .cmd that uses the absolute path to the binary inside the npm package.
286
- // Can't copy bin/sinch.cmd because %~dp0 would resolve to the global bin dir,
287
- // not the package dir where the binary lives.
288
- fs.writeFileSync(path.join(globalBinDir, 'sinch.cmd'), `@echo off\r\n"${binaryPath}" %*\r\n`);
289
-
290
- // Remove .ps1 shim (broken — tries /bin/sh) and bash shim (not needed on Windows)
291
- try {
292
- fs.unlinkSync(path.join(globalBinDir, 'sinch.ps1'));
293
- } catch {}
294
- try {
295
- fs.unlinkSync(path.join(globalBinDir, 'sinch'));
296
- } catch {}
297
-
298
- // Clean up orphaned sinch.exe from previous installToGlobalBin versions
299
- try {
300
- fs.unlinkSync(path.join(globalBinDir, 'sinch.exe'));
301
- } catch {}
302
- } catch {
303
- // Non-fatal — npm's .cmd shim may still work for node-based fallback
304
- }
305
- }
306
-
307
- // --- Main ---
308
-
309
293
  async function main() {
310
- // Only run on global installs or when forced by setup-dev.js
311
294
  if (process.env.npm_config_global !== 'true' && !process.env.SINCH_FORCE_POSTINSTALL) {
312
295
  return;
313
296
  }
@@ -316,14 +299,7 @@ async function main() {
316
299
  fs.mkdirSync(SINCH_DIR, { recursive: true });
317
300
 
318
301
  const version = getVersion();
319
-
320
- // Download the platform binary
321
302
  await downloadBinary(version);
322
-
323
- // Replace npm's broken Windows shims with native .cmd launcher
324
- fixWindowsShims();
325
-
326
- // Set up shell completions
327
303
  writeCompletionsJson(version);
328
304
 
329
305
  if (process.platform === 'win32') {
@@ -331,9 +307,14 @@ async function main() {
331
307
  } else {
332
308
  installBashZshCompletion();
333
309
  }
334
- } catch {
335
- // Silent failure — never break npm install
336
- }
310
+ } catch {}
337
311
  }
338
312
 
339
- main();
313
+ if (require.main === module) {
314
+ main();
315
+ }
316
+
317
+ module.exports = {
318
+ stripManagedBlocks,
319
+ upsertBlock,
320
+ };
package/bin/credman.exe DELETED
Binary file
package/bin/s DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // Shorthand alias — same as bin/sinch
4
- require('./sinch');
package/bin/sinch.cmd DELETED
@@ -1,2 +0,0 @@
1
- @echo off
2
- "%~dp0..\dist\sinch-windows-x64.exe" %*