@sinch/cli 0.3.3 → 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.
- package/README.md +4 -0
- package/dist/index.js +472 -621
- package/package.json +12 -7
- package/scripts/README-E2E.md +242 -0
- package/scripts/e2e-test.sh +155 -0
- package/scripts/post-build.js +36 -0
- package/scripts/postinstall.js +197 -0
- package/scripts/setup-dev.js +189 -0
- package/scripts/smoke-test.js +90 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sinch/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Official Sinch CLI - Manage all Sinch products from your terminal",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"test:smoke": "node scripts/smoke-test.js",
|
|
22
22
|
"setup": "npm run build && npm link && npm run build:binary && node scripts/setup-dev.js",
|
|
23
23
|
"format": "prettier --write .",
|
|
24
|
-
"format:check": "prettier --check ."
|
|
24
|
+
"format:check": "prettier --check .",
|
|
25
|
+
"prepare": "husky"
|
|
25
26
|
},
|
|
26
27
|
"keywords": [],
|
|
27
28
|
"author": "Sinch <support@sinch.com> (https://www.sinch.com)",
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"files": [
|
|
36
37
|
"dist/",
|
|
37
38
|
"bin/",
|
|
39
|
+
"scripts/",
|
|
38
40
|
"README.md",
|
|
39
41
|
"LICENSE"
|
|
40
42
|
],
|
|
@@ -51,26 +53,24 @@
|
|
|
51
53
|
"axios-retry": "^4.5.0",
|
|
52
54
|
"blessed": "^0.1.81",
|
|
53
55
|
"chalk": "^4.1.2",
|
|
54
|
-
"cli-spinners": "^2.9.2",
|
|
55
56
|
"cli-table3": "^0.6.3",
|
|
56
57
|
"clipboardy": "^5.1.0",
|
|
57
58
|
"commander": "^14.0.0",
|
|
58
59
|
"form-data": "^4.0.0",
|
|
59
60
|
"fs-extra": "^11.3.3",
|
|
60
|
-
"google-libphonenumber": "^3.2.44"
|
|
61
|
-
"inquirer": "8.2.7",
|
|
62
|
-
"ora": "^4.1.1"
|
|
61
|
+
"google-libphonenumber": "^3.2.44"
|
|
63
62
|
},
|
|
64
63
|
"devDependencies": {
|
|
65
64
|
"@types/adm-zip": "^0.5.7",
|
|
66
65
|
"@types/blessed": "^0.1.25",
|
|
67
66
|
"@types/fs-extra": "^11.0.4",
|
|
68
67
|
"@types/google-libphonenumber": "^7.4.30",
|
|
69
|
-
"@types/inquirer": "^9.0.9",
|
|
70
68
|
"@types/jest": "^30.0.0",
|
|
71
69
|
"@types/node": "24.10.9",
|
|
72
70
|
"execa": "^5.1.1",
|
|
71
|
+
"husky": "^9.1.7",
|
|
73
72
|
"jest": "^30.0.0",
|
|
73
|
+
"lint-staged": "^16.2.7",
|
|
74
74
|
"nodemon": "^3.0.1",
|
|
75
75
|
"prettier": "^3.8.1",
|
|
76
76
|
"ts-jest": "^29.4.6",
|
|
@@ -81,5 +81,10 @@
|
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": ">=20.0.0",
|
|
83
83
|
"npm": ">=9.0.0"
|
|
84
|
+
},
|
|
85
|
+
"lint-staged": {
|
|
86
|
+
"*.{js,ts,json,md}": [
|
|
87
|
+
"prettier --write"
|
|
88
|
+
]
|
|
84
89
|
}
|
|
85
90
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# E2E Testing Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
End-to-end testing infrastructure for the Sinch CLI that tests the complete workflow against the real API.
|
|
6
|
+
|
|
7
|
+
## What is Tested
|
|
8
|
+
|
|
9
|
+
The E2E test (`e2e-test.sh`) performs the following workflow:
|
|
10
|
+
|
|
11
|
+
1. ✅ **CLI Installation** - Verifies CLI is installed and accessible
|
|
12
|
+
2. ✅ **Authentication** - Checks credentials are configured
|
|
13
|
+
3. ✅ **List Functions** - Verifies API connectivity
|
|
14
|
+
4. ✅ **Init Function** - Creates a new function from template
|
|
15
|
+
5. ✅ **Deploy Function** - Deploys function to production
|
|
16
|
+
6. ✅ **Verify Deployment** - Confirms function is running
|
|
17
|
+
7. ✅ **Check Status** - Validates function status endpoint
|
|
18
|
+
8. ✅ **Check Logs** - Validates log streaming endpoint
|
|
19
|
+
9. ✅ **Delete Function** - Cleans up test resources
|
|
20
|
+
|
|
21
|
+
## Running E2E Tests
|
|
22
|
+
|
|
23
|
+
### Option 1: GitLab CI (Automatic)
|
|
24
|
+
|
|
25
|
+
E2E tests run automatically on:
|
|
26
|
+
|
|
27
|
+
- Every push to `main` branch
|
|
28
|
+
- Every merge request
|
|
29
|
+
|
|
30
|
+
**Required GitLab CI Variables:**
|
|
31
|
+
|
|
32
|
+
- `E2E_KEY_ID` (protected)
|
|
33
|
+
- `E2E_KEY_SECRET` (protected, masked)
|
|
34
|
+
- `E2E_APPLICATION_KEY` (protected)
|
|
35
|
+
- `E2E_APPLICATION_SECRET` (protected, masked)
|
|
36
|
+
|
|
37
|
+
### Option 2: Local with Node.js
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Build and install CLI
|
|
41
|
+
npm run build
|
|
42
|
+
npm install -g .
|
|
43
|
+
|
|
44
|
+
# Set environment variables
|
|
45
|
+
export SINCH_KEY_ID="your-key-id"
|
|
46
|
+
export SINCH_KEY_SECRET="your-key-secret"
|
|
47
|
+
export SINCH_APPLICATION_KEY="your-app-key"
|
|
48
|
+
export SINCH_APPLICATION_SECRET="your-app-secret"
|
|
49
|
+
|
|
50
|
+
# Run E2E test
|
|
51
|
+
./scripts/e2e-test.sh
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Option 3: Docker (Isolated Environment)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Build E2E Docker image
|
|
58
|
+
docker build -f Dockerfile.e2e -t sinch-cli-e2e .
|
|
59
|
+
|
|
60
|
+
# Run E2E tests in Docker
|
|
61
|
+
docker run --rm \
|
|
62
|
+
-e SINCH_KEY_ID="your-key-id" \
|
|
63
|
+
-e SINCH_KEY_SECRET="your-key-secret" \
|
|
64
|
+
-e SINCH_APPLICATION_KEY="your-app-key" \
|
|
65
|
+
-e SINCH_APPLICATION_SECRET="your-app-secret" \
|
|
66
|
+
sinch-cli-e2e
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Docker Benefits:**
|
|
70
|
+
|
|
71
|
+
- Clean, isolated environment every run
|
|
72
|
+
- No local Node.js required
|
|
73
|
+
- Same environment as CI
|
|
74
|
+
- Great for testing before pushing
|
|
75
|
+
|
|
76
|
+
## Test Output
|
|
77
|
+
|
|
78
|
+
The test script provides colored output:
|
|
79
|
+
|
|
80
|
+
- ✅ **Green** - Test passed
|
|
81
|
+
- ❌ **Red** - Test failed
|
|
82
|
+
- ⚠️ **Yellow** - Warning (non-fatal)
|
|
83
|
+
|
|
84
|
+
Example output:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
=== Sinch CLI E2E Test ===
|
|
88
|
+
|
|
89
|
+
Test 1: CLI installation check
|
|
90
|
+
✅ PASS: CLI installed (version: 0.1.0)
|
|
91
|
+
|
|
92
|
+
Test 2: Authentication check
|
|
93
|
+
✅ PASS: Authentication OK
|
|
94
|
+
|
|
95
|
+
Test 3: List functions
|
|
96
|
+
✅ PASS: List functions OK
|
|
97
|
+
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
=== E2E Test Complete ===
|
|
101
|
+
✅ All tests passed!
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Credentials
|
|
105
|
+
|
|
106
|
+
### CI/CD Environment
|
|
107
|
+
|
|
108
|
+
In GitLab CI, credentials are provided via environment variables configured in GitLab:
|
|
109
|
+
|
|
110
|
+
Settings → CI/CD → Variables
|
|
111
|
+
|
|
112
|
+
### Local Development
|
|
113
|
+
|
|
114
|
+
Two options:
|
|
115
|
+
|
|
116
|
+
1. **Environment Variables** (recommended for E2E):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
export SINCH_KEY_ID="..."
|
|
120
|
+
export SINCH_KEY_SECRET="..."
|
|
121
|
+
export SINCH_APPLICATION_KEY="..."
|
|
122
|
+
export SINCH_APPLICATION_SECRET="..."
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
2. **OS Keychain** (normal CLI usage):
|
|
126
|
+
```bash
|
|
127
|
+
sinch auth login
|
|
128
|
+
# Credentials stored securely in OS keychain
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Troubleshooting
|
|
132
|
+
|
|
133
|
+
### Test Fails: Authentication not configured
|
|
134
|
+
|
|
135
|
+
**Problem:** Environment variables not set
|
|
136
|
+
|
|
137
|
+
**Solution:**
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Check env vars are set
|
|
141
|
+
echo $SINCH_KEY_ID
|
|
142
|
+
echo $SINCH_APPLICATION_KEY
|
|
143
|
+
|
|
144
|
+
# Set them if missing
|
|
145
|
+
export SINCH_KEY_ID="..."
|
|
146
|
+
export SINCH_KEY_SECRET="..."
|
|
147
|
+
export SINCH_APPLICATION_KEY="..."
|
|
148
|
+
export SINCH_APPLICATION_SECRET="..."
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Test Fails: Deploy timeout
|
|
152
|
+
|
|
153
|
+
**Problem:** API is slow or function deployment taking longer than expected
|
|
154
|
+
|
|
155
|
+
**Solution:** The test allows up to 2 minutes for deployment. If it times out:
|
|
156
|
+
|
|
157
|
+
1. Check API status
|
|
158
|
+
2. Check function complexity
|
|
159
|
+
3. Try again (might be temporary)
|
|
160
|
+
|
|
161
|
+
### Test Fails: Function not found after delete
|
|
162
|
+
|
|
163
|
+
**Problem:** This is actually expected - the test verifies deletion worked
|
|
164
|
+
|
|
165
|
+
**Solution:** No action needed, this is correct behavior
|
|
166
|
+
|
|
167
|
+
## Cleanup
|
|
168
|
+
|
|
169
|
+
The E2E test **always cleans up** test functions, even on failure:
|
|
170
|
+
|
|
171
|
+
- Uses a `cleanup()` trap function
|
|
172
|
+
- Runs on EXIT, INT (Ctrl+C), or TERM signals
|
|
173
|
+
- Deletes test function by ID
|
|
174
|
+
- Safe to run multiple times
|
|
175
|
+
|
|
176
|
+
## CI/CD Integration
|
|
177
|
+
|
|
178
|
+
### Pipeline Stages
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
┌─────────┐
|
|
182
|
+
│ Build │ (26s)
|
|
183
|
+
└────┬────┘
|
|
184
|
+
│
|
|
185
|
+
┌────▼────┐
|
|
186
|
+
│ Test:E2E│ (13s) ← New!
|
|
187
|
+
└────┬────┘
|
|
188
|
+
│
|
|
189
|
+
┌────▼────┐
|
|
190
|
+
│ Publish │ (14s)
|
|
191
|
+
└─────────┘
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### When Tests Run
|
|
195
|
+
|
|
196
|
+
- **Main branch:** Every commit
|
|
197
|
+
- **Merge requests:** Every push to MR
|
|
198
|
+
- **Tags:** No (only build + publish)
|
|
199
|
+
|
|
200
|
+
### Failure Handling
|
|
201
|
+
|
|
202
|
+
- `allow_failure: false` - Pipeline **fails** if E2E test fails
|
|
203
|
+
- Prevents broken code from being published
|
|
204
|
+
- Forces fixes before merge
|
|
205
|
+
|
|
206
|
+
## Best Practices
|
|
207
|
+
|
|
208
|
+
1. **Run E2E before pushing:**
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm run build
|
|
212
|
+
npm install -g .
|
|
213
|
+
./scripts/e2e-test.sh
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
2. **Use Docker for final verification:**
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
docker build -f Dockerfile.e2e -t sinch-cli-e2e .
|
|
220
|
+
docker run --rm -e SINCH_KEY_ID="..." ... sinch-cli-e2e
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
3. **Watch CI pipeline:**
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
glab ci view
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
4. **Never commit with failing E2E tests** - The pipeline will block you anyway
|
|
230
|
+
|
|
231
|
+
## Files
|
|
232
|
+
|
|
233
|
+
- `scripts/e2e-test.sh` - Main E2E test script
|
|
234
|
+
- `Dockerfile.e2e` - Docker image for isolated testing
|
|
235
|
+
- `.gitlab-ci.yml` - CI pipeline configuration
|
|
236
|
+
- `scripts/README-E2E.md` - This documentation
|
|
237
|
+
|
|
238
|
+
## Support
|
|
239
|
+
|
|
240
|
+
- **Issues:** Report at GitLab project issues
|
|
241
|
+
- **CI Logs:** View in GitLab CI pipeline
|
|
242
|
+
- **Local Debugging:** Add `set -x` to script for verbose output
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -e # Exit on error
|
|
3
|
+
|
|
4
|
+
echo "=== Sinch CLI E2E Test ==="
|
|
5
|
+
echo ""
|
|
6
|
+
|
|
7
|
+
# Generate unique test name
|
|
8
|
+
TEST_NAME="e2e-test"
|
|
9
|
+
FUNCTION_ID=""
|
|
10
|
+
FAILED=0
|
|
11
|
+
|
|
12
|
+
# Colors for output
|
|
13
|
+
RED='\033[0;31m'
|
|
14
|
+
GREEN='\033[0;32m'
|
|
15
|
+
YELLOW='\033[1;33m'
|
|
16
|
+
NC='\033[0m' # No Color
|
|
17
|
+
|
|
18
|
+
# Function to cleanup on exit
|
|
19
|
+
cleanup() {
|
|
20
|
+
EXIT_CODE=$?
|
|
21
|
+
|
|
22
|
+
if [ -n "$FUNCTION_ID" ]; then
|
|
23
|
+
echo ""
|
|
24
|
+
echo "${YELLOW}Cleaning up test function: $FUNCTION_ID${NC}"
|
|
25
|
+
sinch functions delete "$FUNCTION_ID" --force 2>/dev/null || true
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# If script exited with error and FAILED wasn't set, set it now
|
|
29
|
+
if [ $EXIT_CODE -ne 0 ] && [ $FAILED -eq 0 ]; then
|
|
30
|
+
FAILED=$EXIT_CODE
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if [ $FAILED -eq 0 ]; then
|
|
34
|
+
echo ""
|
|
35
|
+
echo "${GREEN}=== E2E Test Complete ===${NC}"
|
|
36
|
+
echo "${GREEN}All tests passed!${NC}"
|
|
37
|
+
else
|
|
38
|
+
echo ""
|
|
39
|
+
echo "${RED}=== E2E Test Failed ===${NC}"
|
|
40
|
+
echo "${RED}Exit code: $FAILED${NC}"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
exit $FAILED
|
|
44
|
+
}
|
|
45
|
+
trap cleanup EXIT INT TERM
|
|
46
|
+
|
|
47
|
+
# Test 1: Check CLI is installed
|
|
48
|
+
echo "Test 1: CLI installation check"
|
|
49
|
+
if ! command -v sinch >/dev/null 2>&1; then
|
|
50
|
+
echo "${RED}FAIL: sinch command not found${NC}"
|
|
51
|
+
FAILED=1
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
CLI_VERSION=$(sinch --version)
|
|
55
|
+
echo "${GREEN}PASS: CLI installed (version: $CLI_VERSION)${NC}"
|
|
56
|
+
echo ""
|
|
57
|
+
|
|
58
|
+
# Test 2: List functions (will fail if auth not configured via env vars)
|
|
59
|
+
echo "Test 2: List functions"
|
|
60
|
+
if ! sinch functions list >/dev/null 2>&1; then
|
|
61
|
+
echo "${RED}FAIL: Cannot list functions${NC}"
|
|
62
|
+
echo "Make sure these environment variables are set:"
|
|
63
|
+
echo " - SINCH_KEY_ID"
|
|
64
|
+
echo " - SINCH_KEY_SECRET"
|
|
65
|
+
echo " - SINCH_APPLICATION_KEY"
|
|
66
|
+
echo " - SINCH_APPLICATION_SECRET"
|
|
67
|
+
FAILED=1
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
echo "${GREEN}PASS: List functions OK${NC}"
|
|
71
|
+
echo ""
|
|
72
|
+
|
|
73
|
+
# Test 3: Init function
|
|
74
|
+
echo "Test 3: Initialize function"
|
|
75
|
+
TEST_DIR="/tmp/$TEST_NAME"
|
|
76
|
+
cd /tmp
|
|
77
|
+
|
|
78
|
+
if ! sinch functions init simple-voice-ivr \
|
|
79
|
+
--name "$TEST_NAME" \
|
|
80
|
+
--skip-install \
|
|
81
|
+
--non-interactive >/dev/null 2>&1; then
|
|
82
|
+
echo "${RED}FAIL: Function init failed${NC}"
|
|
83
|
+
FAILED=1
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
echo "${GREEN}PASS: Function initialized${NC}"
|
|
87
|
+
echo ""
|
|
88
|
+
|
|
89
|
+
# Test 4: Deploy function
|
|
90
|
+
echo "Test 4: Deploy function (this may take a minute...)"
|
|
91
|
+
cd "$TEST_NAME" || {
|
|
92
|
+
echo "${RED}FAIL: Cannot change to function directory${NC}"
|
|
93
|
+
FAILED=1
|
|
94
|
+
exit 1
|
|
95
|
+
}
|
|
96
|
+
DEPLOY_OUTPUT=$(sinch functions deploy --non-interactive 2>&1) || {
|
|
97
|
+
echo "${RED}FAIL: Deploy command failed${NC}"
|
|
98
|
+
echo "$DEPLOY_OUTPUT"
|
|
99
|
+
FAILED=1
|
|
100
|
+
exit 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Extract function ID from deploy output
|
|
104
|
+
FUNCTION_ID=$(echo "$DEPLOY_OUTPUT" | grep "Function ID:" | awk '{print $NF}')
|
|
105
|
+
if [ -z "$FUNCTION_ID" ]; then
|
|
106
|
+
echo "${RED}FAIL: Deploy succeeded but no function ID found${NC}"
|
|
107
|
+
echo "$DEPLOY_OUTPUT"
|
|
108
|
+
FAILED=1
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
echo "${GREEN}PASS: Function deployed (ID: $FUNCTION_ID)${NC}"
|
|
112
|
+
echo ""
|
|
113
|
+
|
|
114
|
+
# Test 5: Verify deployment
|
|
115
|
+
echo "Test 5: Verify function is accessible"
|
|
116
|
+
sleep 2 # Give it a moment to be fully available
|
|
117
|
+
if ! sinch functions list | grep -q "$TEST_NAME"; then
|
|
118
|
+
echo "${RED}FAIL: Function not found in list${NC}"
|
|
119
|
+
FAILED=1
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
echo "${GREEN}PASS: Function is accessible${NC}"
|
|
123
|
+
echo ""
|
|
124
|
+
|
|
125
|
+
# Test 6: Check function status
|
|
126
|
+
echo "Test 6: Check function status"
|
|
127
|
+
if ! sinch functions status "$FUNCTION_ID" >/dev/null 2>&1; then
|
|
128
|
+
echo "${YELLOW}WARN: Cannot fetch function status (non-fatal)${NC}"
|
|
129
|
+
else
|
|
130
|
+
echo "${GREEN}PASS: Function status OK${NC}"
|
|
131
|
+
fi
|
|
132
|
+
echo ""
|
|
133
|
+
|
|
134
|
+
# Test 7: Check logs are accessible
|
|
135
|
+
echo "Test 7: Check logs are accessible"
|
|
136
|
+
if ! sinch functions logs "$FUNCTION_ID" --limit 10 >/dev/null 2>&1; then
|
|
137
|
+
echo "${YELLOW}WARN: Cannot fetch logs (non-fatal)${NC}"
|
|
138
|
+
else
|
|
139
|
+
echo "${GREEN}PASS: Logs accessible${NC}"
|
|
140
|
+
fi
|
|
141
|
+
echo ""
|
|
142
|
+
|
|
143
|
+
# Test 8: Delete function (cleanup will also handle this)
|
|
144
|
+
echo "Test 8: Delete function"
|
|
145
|
+
if ! sinch functions delete "$FUNCTION_ID" --force >/dev/null 2>&1; then
|
|
146
|
+
echo "${RED}FAIL: Delete failed${NC}"
|
|
147
|
+
FAILED=1
|
|
148
|
+
exit 1
|
|
149
|
+
fi
|
|
150
|
+
echo "${GREEN}PASS: Function deleted${NC}"
|
|
151
|
+
FUNCTION_ID="" # Clear so cleanup doesn't try again
|
|
152
|
+
echo ""
|
|
153
|
+
|
|
154
|
+
# All tests passed
|
|
155
|
+
exit 0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
async function postBuild() {
|
|
7
|
+
console.log('Running post-build tasks...');
|
|
8
|
+
|
|
9
|
+
// Ensure dist directory exists
|
|
10
|
+
await fs.ensureDir('dist');
|
|
11
|
+
|
|
12
|
+
// Copy package.json to dist for runtime requires
|
|
13
|
+
await fs.copy('package.json', 'dist/package.json');
|
|
14
|
+
|
|
15
|
+
// Copy skills folder to dist
|
|
16
|
+
const skillsSource = path.join(__dirname, '..', 'skills');
|
|
17
|
+
const skillsDest = path.join(__dirname, '..', 'dist', 'skills');
|
|
18
|
+
if (await fs.pathExists(skillsSource)) {
|
|
19
|
+
await fs.copy(skillsSource, skillsDest, { overwrite: true });
|
|
20
|
+
console.log('Copied skills to dist/skills');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Ensure the CLI entry point works
|
|
24
|
+
const distIndex = path.join('dist', 'index.js');
|
|
25
|
+
if (await fs.pathExists(distIndex)) {
|
|
26
|
+
// Add shebang if not present
|
|
27
|
+
const content = await fs.readFile(distIndex, 'utf8');
|
|
28
|
+
if (!content.startsWith('#!/usr/bin/env node')) {
|
|
29
|
+
await fs.writeFile(distIndex, '#!/usr/bin/env node\n' + content);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('Post-build tasks completed.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
postBuild().catch(console.error);
|
|
@@ -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();
|