@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 +33 -5
- package/bin/platform-resolver.js +59 -32
- package/bin/sinch +1 -1
- package/package.json +9 -5
- package/bin/credman.exe +0 -0
- package/scripts/postinstall.js +0 -341
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
|
|
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)
|
package/bin/platform-resolver.js
CHANGED
|
@@ -1,61 +1,88 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
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
|
|
18
|
-
return
|
|
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,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'
|
|
31
|
-
|
|
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
|
-
|
|
50
|
+
basedir = __dirname,
|
|
36
51
|
platform = process.platform,
|
|
37
52
|
arch = process.arch
|
|
38
53
|
) {
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
45
|
-
if (!
|
|
46
|
-
throw new Error(buildMissingBinaryMessage(platform, arch,
|
|
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
|
-
|
|
83
|
+
PLATFORM_PACKAGES,
|
|
57
84
|
getPlatformKey,
|
|
58
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
10
|
+
"bin/sinch",
|
|
11
|
+
"bin/platform-resolver.js",
|
|
12
12
|
"README.md",
|
|
13
13
|
"LICENSE"
|
|
14
14
|
],
|
|
15
|
-
"
|
|
16
|
-
"
|
|
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
|
package/scripts/postinstall.js
DELETED
|
@@ -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
|
-
};
|