@sinch/cli 0.3.2 → 0.3.4

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,197 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * postinstall.js — runs on `npm install -g @sinch/cli`
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`.
10
+ *
11
+ * Keep COMPLETION_COMMANDS in sync with src/index.ts.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+ const { spawn } = require('child_process');
18
+
19
+ const SINCH_DIR = path.join(os.homedir(), '.sinch');
20
+
21
+ // Keep in sync with COMPLETION_COMMANDS in src/index.ts.
22
+ // The postAction hook overwrites completions.json on every CLI run, so drift self-heals.
23
+ const COMPLETION_COMMANDS = {
24
+ functions: ['init', 'list', 'deploy', 'download', 'dev', 'status', 'logs', 'delete', 'docs'],
25
+ templates: ['list', 'show', 'node', 'csharp', 'python'],
26
+ voice: ['callback-url', 'get-callbacks', 'set-callback'],
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
+ config: ['--set', '--get', '--list'],
35
+ completion: ['--shell', '--install'],
36
+ };
37
+
38
+ const SENTINEL_START = '# ── Sinch CLI completion ──';
39
+ const SENTINEL_END = '# ── End Sinch CLI completion ──';
40
+
41
+ function getVersion() {
42
+ try {
43
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
44
+ return pkg.version || '0.0.0';
45
+ } catch {
46
+ return '0.0.0';
47
+ }
48
+ }
49
+
50
+ function writeCompletionsJson(version) {
51
+ const filePath = path.join(SINCH_DIR, 'completions.json');
52
+ fs.writeFileSync(filePath, JSON.stringify({ version, commands: COMPLETION_COMMANDS }, null, 2));
53
+ }
54
+
55
+ function getPowerShellCompletionScript() {
56
+ return `${SENTINEL_START}
57
+ # PowerShell completion for Sinch CLI
58
+ # Reads command tree from ~/.sinch/completions.json (auto-updated by CLI)
59
+
60
+ Register-ArgumentCompleter -Native -CommandName sinch -ScriptBlock {
61
+ param($wordToComplete, $commandAst, $cursorPosition)
62
+
63
+ try {
64
+ $jsonPath = Join-Path $env:USERPROFILE '.sinch' 'completions.json'
65
+ if (-not (Test-Path $jsonPath)) { return }
66
+
67
+ $data = Get-Content $jsonPath -Raw | ConvertFrom-Json
68
+ $line = $commandAst.CommandElements
69
+ $command = if ($line.Count -gt 1) { $line[1].Value } else { "" }
70
+
71
+ if ($line.Count -eq 2) {
72
+ $mainCommands = @($data.commands.PSObject.Properties.Name) + @('--version', '--help')
73
+ $mainCommands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
74
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
75
+ }
76
+ }
77
+ elseif ($line.Count -eq 3 -and $data.commands.$command) {
78
+ @($data.commands.$command) | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
79
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
80
+ }
81
+ }
82
+ }
83
+ catch {
84
+ # Silently fail — prevents PowerShell startup errors
85
+ }
86
+ }
87
+ ${SENTINEL_END}`;
88
+ }
89
+
90
+ // --- Profile installation helpers ---
91
+
92
+ function shellEscapePath(p) {
93
+ return "'" + p.replace(/'/g, "'\\''") + "'";
94
+ }
95
+
96
+ function upsertBlock(content, block) {
97
+ const pattern =
98
+ SENTINEL_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
99
+ '[\\s\\S]*?' +
100
+ SENTINEL_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
101
+ const replaced = content.replace(new RegExp(pattern, 'g'), block.trim());
102
+ if (replaced !== content) return replaced;
103
+ return content + (content && !content.endsWith('\n') ? '\n' : '') + '\n' + block.trim() + '\n';
104
+ }
105
+
106
+ function tryShell(cmd) {
107
+ return new Promise((resolve) => {
108
+ const ps = spawn(cmd, ['-NoProfile', '-Command', '$PROFILE'], {
109
+ stdio: ['ignore', 'pipe', 'pipe'],
110
+ });
111
+ let out = '';
112
+ ps.stdout.on('data', (d) => (out += d.toString()));
113
+ ps.on('close', (code) => resolve(code === 0 ? out.trim() : null));
114
+ ps.on('error', () => resolve(null));
115
+ });
116
+ }
117
+
118
+ async function installPowerShellCompletion() {
119
+ const completionFile = path.join(SINCH_DIR, 'sinch-completion.ps1');
120
+ fs.writeFileSync(completionFile, getPowerShellCompletionScript());
121
+
122
+ const profilePath = (await tryShell('pwsh')) ?? (await tryShell('powershell'));
123
+ if (!profilePath) return;
124
+
125
+ // Validate profile path is within user's home directory
126
+ const resolved = path.resolve(profilePath);
127
+ if (!resolved.startsWith(os.homedir())) return;
128
+
129
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
130
+
131
+ let content = '';
132
+ try {
133
+ content = fs.readFileSync(resolved, 'utf8');
134
+ } catch {
135
+ // Profile doesn't exist yet — will be created
136
+ }
137
+
138
+ const sourceLine = `. ${shellEscapePath(completionFile.replace(/\\/g, '\\\\'))}`;
139
+ const block = `${SENTINEL_START}\n${sourceLine}\n${SENTINEL_END}`;
140
+ fs.writeFileSync(resolved, upsertBlock(content, block));
141
+ }
142
+
143
+ function installBashZshCompletion() {
144
+ const completionFile = path.join(SINCH_DIR, 'sinch-completion.bash');
145
+ const sourceLine = `source ${shellEscapePath(completionFile)}`;
146
+ const block = `${SENTINEL_START}\n${sourceLine}\n${SENTINEL_END}`;
147
+
148
+ const home = os.homedir();
149
+ const zshrc = path.join(home, '.zshrc');
150
+ const bashrc = path.join(home, '.bashrc');
151
+
152
+ const rcFiles = [];
153
+ if (process.platform === 'darwin') {
154
+ rcFiles.push(zshrc);
155
+ if (fs.existsSync(bashrc)) rcFiles.push(bashrc);
156
+ } else {
157
+ if (fs.existsSync(bashrc)) rcFiles.push(bashrc);
158
+ if (fs.existsSync(zshrc)) rcFiles.push(zshrc);
159
+ if (rcFiles.length === 0) rcFiles.push(bashrc);
160
+ }
161
+
162
+ for (const rcFile of rcFiles) {
163
+ let content = '';
164
+ try {
165
+ content = fs.readFileSync(rcFile, 'utf8');
166
+ } catch {
167
+ // File doesn't exist yet — will be created
168
+ }
169
+ fs.writeFileSync(rcFile, upsertBlock(content, block));
170
+ }
171
+ }
172
+
173
+ // --- Main ---
174
+
175
+ async function main() {
176
+ // Only run on global installs or when forced by setup-dev.js
177
+ if (process.env.npm_config_global !== 'true' && !process.env.SINCH_FORCE_POSTINSTALL) {
178
+ return;
179
+ }
180
+
181
+ try {
182
+ fs.mkdirSync(SINCH_DIR, { recursive: true });
183
+
184
+ const version = getVersion();
185
+ writeCompletionsJson(version);
186
+
187
+ if (process.platform === 'win32') {
188
+ await installPowerShellCompletion();
189
+ } else {
190
+ installBashZshCompletion();
191
+ }
192
+ } catch {
193
+ // Silent failure — never break npm install
194
+ }
195
+ }
196
+
197
+ main();
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync, spawn } = require('child_process');
7
+
8
+ const SINCH_BIN = path.join(os.homedir(), '.sinch', 'bin');
9
+ const BINARY_NAME = process.platform === 'win32' ? 'sinch.exe' : 'sinch';
10
+ const BINARY_SRC = path.join(__dirname, '..', 'dist', 'sinch-bun.exe');
11
+ const BINARY_DEST = path.join(SINCH_BIN, BINARY_NAME);
12
+
13
+ async function setup() {
14
+ console.log('\n🔧 Setting up Sinch CLI development environment\n');
15
+
16
+ // 1. Copy Bun binary to ~/.sinch/bin/
17
+ console.log('📦 Installing Bun binary...');
18
+ await fs.ensureDir(SINCH_BIN);
19
+
20
+ if (await fs.pathExists(BINARY_SRC)) {
21
+ await fs.copy(BINARY_SRC, BINARY_DEST, { overwrite: true });
22
+ if (process.platform !== 'win32') {
23
+ await fs.chmod(BINARY_DEST, 0o755);
24
+ }
25
+ console.log(` ✅ Installed to ${BINARY_DEST}`);
26
+ } else {
27
+ console.log(' ⚠️ Bun binary not found — run "npm run build:binary" first');
28
+ console.log(` Expected at: ${BINARY_SRC}`);
29
+ }
30
+
31
+ // 2. Generate PowerShell profile additions
32
+ if (process.platform === 'win32') {
33
+ await setupPowerShell();
34
+ } else {
35
+ await setupShell();
36
+ }
37
+
38
+ // 3. Install shell completions
39
+ console.log('\n🔧 Installing shell completions...');
40
+ try {
41
+ await installCompletions();
42
+ console.log(' ✅ Completions installed');
43
+ } catch (e) {
44
+ console.log(` ⚠️ Completion install failed: ${e.message}`);
45
+ }
46
+
47
+ // 4. Smoke test
48
+ console.log('\n🧪 Smoke test...');
49
+ try {
50
+ const version = execSync(`"${BINARY_DEST}" --version`, { encoding: 'utf8' }).trim();
51
+ console.log(` ✅ sinch binary v${version}`);
52
+ } catch (e) {
53
+ console.log(` ⚠️ Binary smoke test failed: ${e.message}`);
54
+ }
55
+
56
+ try {
57
+ const devVersion = execSync('sinch --version', { encoding: 'utf8' }).trim();
58
+ console.log(` ✅ sinch dev (npm link) v${devVersion}`);
59
+ } catch (e) {
60
+ console.log(` ⚠️ npm link not set up: ${e.message}`);
61
+ }
62
+
63
+ console.log('\n✨ Setup complete!\n');
64
+ console.log('Commands:');
65
+ console.log(' s → Fast Bun binary (for daily use)');
66
+ console.log(' sf <cmd> → Shortcut for "s functions <cmd>"');
67
+ console.log(' sd <cmd> → Dev version via npm link (for CLI development)');
68
+ console.log(' sinch <cmd> → Same as sd (npm-linked)');
69
+ console.log('');
70
+
71
+ if (process.platform === 'win32') {
72
+ console.log('⚡ Restart PowerShell or run: . $PROFILE');
73
+ } else {
74
+ console.log('⚡ Restart your shell or run: source ~/.bashrc');
75
+ }
76
+ console.log('');
77
+ }
78
+
79
+ async function setupPowerShell() {
80
+ console.log('\n🖥️ Setting up PowerShell aliases...');
81
+
82
+ const profileSnippet = `
83
+ # ── Sinch CLI aliases (auto-generated by sinch-cli setup) ──
84
+ function s { & "$env:USERPROFILE\\.sinch\\bin\\sinch.exe" @args }
85
+ function sf { & "$env:USERPROFILE\\.sinch\\bin\\sinch.exe" functions @args }
86
+ function sd { & sinch @args }
87
+ # ── End Sinch CLI aliases ──
88
+ `.trim();
89
+
90
+ // Find PowerShell profile
91
+ const profilePath = await getPowerShellProfilePath();
92
+
93
+ if (!profilePath) {
94
+ console.log(' ⚠️ Could not detect PowerShell profile path');
95
+ console.log(' Add these to your PowerShell profile manually:\n');
96
+ console.log(profileSnippet);
97
+ return;
98
+ }
99
+
100
+ // Read existing profile
101
+ let profileContent = '';
102
+ if (await fs.pathExists(profilePath)) {
103
+ profileContent = await fs.readFile(profilePath, 'utf8');
104
+ }
105
+
106
+ // Check if already installed
107
+ if (profileContent.includes('Sinch CLI aliases')) {
108
+ // Replace existing block
109
+ const replaced = profileContent.replace(
110
+ /# ── Sinch CLI aliases.*?# ── End Sinch CLI aliases ──/s,
111
+ profileSnippet
112
+ );
113
+ await fs.writeFile(profilePath, replaced);
114
+ console.log(` ✅ Updated aliases in ${profilePath}`);
115
+ } else {
116
+ // Append
117
+ await fs.writeFile(profilePath, profileContent + '\n\n' + profileSnippet + '\n');
118
+ console.log(` ✅ Added aliases to ${profilePath}`);
119
+ }
120
+ }
121
+
122
+ async function setupShell() {
123
+ console.log('\n🖥️ Setting up shell aliases...');
124
+
125
+ const snippet = `
126
+ # ── Sinch CLI aliases (auto-generated by sinch-cli setup) ──
127
+ alias s='$HOME/.sinch/bin/sinch'
128
+ sf() { $HOME/.sinch/bin/sinch functions "$@"; }
129
+ alias sd='sinch'
130
+ # ── End Sinch CLI aliases ──
131
+ `.trim();
132
+
133
+ // Try .bashrc, then .zshrc
134
+ const rcFile = (await fs.pathExists(path.join(os.homedir(), '.zshrc')))
135
+ ? path.join(os.homedir(), '.zshrc')
136
+ : path.join(os.homedir(), '.bashrc');
137
+
138
+ let content = '';
139
+ if (await fs.pathExists(rcFile)) {
140
+ content = await fs.readFile(rcFile, 'utf8');
141
+ }
142
+
143
+ if (content.includes('Sinch CLI aliases')) {
144
+ const replaced = content.replace(
145
+ /# ── Sinch CLI aliases.*?# ── End Sinch CLI aliases ──/s,
146
+ snippet
147
+ );
148
+ await fs.writeFile(rcFile, replaced);
149
+ console.log(` ✅ Updated aliases in ${rcFile}`);
150
+ } else {
151
+ await fs.writeFile(rcFile, content + '\n\n' + snippet + '\n');
152
+ console.log(` ✅ Added aliases to ${rcFile}`);
153
+ }
154
+ }
155
+
156
+ async function installCompletions() {
157
+ execSync('node scripts/postinstall.js', {
158
+ cwd: path.join(__dirname, '..'),
159
+ env: { ...process.env, SINCH_FORCE_POSTINSTALL: '1' },
160
+ stdio: 'pipe',
161
+ });
162
+ }
163
+
164
+ function getPowerShellProfilePath() {
165
+ return new Promise((resolve) => {
166
+ // Try pwsh first, then powershell
167
+ const tryShell = (cmd) => {
168
+ return new Promise((res) => {
169
+ const ps = spawn(cmd, ['-NoProfile', '-Command', '$PROFILE'], {
170
+ stdio: ['ignore', 'pipe', 'pipe'],
171
+ });
172
+ let out = '';
173
+ ps.stdout.on('data', (d) => (out += d.toString()));
174
+ ps.on('close', (code) => res(code === 0 ? out.trim() : null));
175
+ ps.on('error', () => res(null));
176
+ });
177
+ };
178
+
179
+ tryShell('pwsh').then((p) => {
180
+ if (p) return resolve(p);
181
+ tryShell('powershell').then((p2) => resolve(p2));
182
+ });
183
+ });
184
+ }
185
+
186
+ setup().catch((err) => {
187
+ console.error('Setup failed:', err.message);
188
+ process.exit(1);
189
+ });
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Smoke test for the Bun-compiled binary.
5
+ * Verifies the binary works for key code paths that could differ from Node.js.
6
+ */
7
+
8
+ const { execSync } = require('child_process');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const rawBinary =
13
+ process.argv[2] ||
14
+ path.join(os.homedir(), '.sinch', 'bin', process.platform === 'win32' ? 'sinch.exe' : 'sinch');
15
+ const BINARY = path.resolve(rawBinary);
16
+
17
+ let passed = 0;
18
+ let failed = 0;
19
+
20
+ function test(name, fn) {
21
+ try {
22
+ fn();
23
+ console.log(` ✅ ${name}`);
24
+ passed++;
25
+ } catch (e) {
26
+ console.log(` ❌ ${name}: ${e.message}`);
27
+ failed++;
28
+ }
29
+ }
30
+
31
+ function run(args) {
32
+ return execSync(`"${BINARY}" ${args}`, {
33
+ encoding: 'utf8',
34
+ timeout: 10000,
35
+ env: { ...process.env, CI: '1' },
36
+ }).trim();
37
+ }
38
+
39
+ console.log(`\n🧪 Smoke testing: ${BINARY}\n`);
40
+
41
+ test('--version returns semver', () => {
42
+ const version = run('--version');
43
+ if (!/^\d+\.\d+\.\d+/.test(version)) {
44
+ throw new Error(`Got: ${version}`);
45
+ }
46
+ });
47
+
48
+ test('--help shows all top-level commands', () => {
49
+ const help = run('--help');
50
+ const required = ['functions', 'templates', 'voice', 'auth', 'config', 'fax', 'sip', 'numbers'];
51
+ for (const cmd of required) {
52
+ if (!help.includes(cmd)) {
53
+ throw new Error(`Missing command: ${cmd}`);
54
+ }
55
+ }
56
+ });
57
+
58
+ test('functions --help shows subcommands', () => {
59
+ const help = run('functions --help');
60
+ const required = ['init', 'list', 'deploy', 'dev', 'status', 'logs'];
61
+ for (const cmd of required) {
62
+ if (!help.includes(cmd)) {
63
+ throw new Error(`Missing subcommand: ${cmd}`);
64
+ }
65
+ }
66
+ });
67
+
68
+ test('fax --help shows subcommands', () => {
69
+ const help = run('fax --help');
70
+ if (!help.includes('send') || !help.includes('list')) {
71
+ throw new Error('Missing fax subcommands');
72
+ }
73
+ });
74
+
75
+ test('sip --help shows subcommands', () => {
76
+ const help = run('sip --help');
77
+ if (!help.includes('trunks') || !help.includes('endpoints')) {
78
+ throw new Error('Missing sip subcommands');
79
+ }
80
+ });
81
+
82
+ test('config --help shows subcommands', () => {
83
+ const help = run('config --help');
84
+ if (!help.includes('set') || !help.includes('get') || !help.includes('profile')) {
85
+ throw new Error('Missing config subcommands');
86
+ }
87
+ });
88
+
89
+ console.log(`\n${passed} passed, ${failed} failed\n`);
90
+ process.exit(failed > 0 ? 1 : 0);
package/dist/index.d.ts DELETED
@@ -1 +0,0 @@
1
- #!/usr/bin/env node