servyx-cli 0.1.10 → 0.1.12
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 +0 -3
- package/package.json +1 -1
- package/src/commands/deploy.js +4 -2
- package/src/index.js +2 -1
- package/src/lib/api.js +162 -11
package/README.md
CHANGED
|
@@ -61,9 +61,6 @@ servyx status
|
|
|
61
61
|
- Project config: `servyx.json` in your project root
|
|
62
62
|
- Optional excludes: `.servyxignore` (rsync patterns). By default, hidden files (`.env`, `.htaccess`, etc.) are not uploaded and existing server copies are preserved. Use `servyx deploy --with-env` to upload local `.env` / `.env.*` files (useful for React/Vite builds).
|
|
63
63
|
|
|
64
|
-
Environment variables:
|
|
65
|
-
|
|
66
|
-
- `SERVYX_API_URL` — override CLI service base URL
|
|
67
64
|
|
|
68
65
|
## How it works
|
|
69
66
|
|
package/package.json
CHANGED
package/src/commands/deploy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
-
import { getDeployTarget, getDeployment, startDeployment } from '../lib/api.js';
|
|
2
|
+
import { getDeployTarget, getDeployment, startDeployment, setApiVerbose } from '../lib/api.js';
|
|
3
3
|
import { getToken } from '../lib/config.js';
|
|
4
4
|
import { loadProjectConfig, loadServyxIgnore } from '../lib/project.js';
|
|
5
5
|
import { rsyncDeploy, sshPreflight, DEFAULT_RSYNC_EXCLUDES, preserveServerDotfiles, ensureRemoteDir, verifyRemoteStagingDir } from '../lib/rsync.js';
|
|
@@ -18,6 +18,8 @@ function formatDeploymentLine(deployment) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export async function runDeploy(options = {}) {
|
|
21
|
+
setApiVerbose(Boolean(options.verbose));
|
|
22
|
+
|
|
21
23
|
if (!getToken()) {
|
|
22
24
|
console.error(pc.red('Not logged in. Run: servyx login'));
|
|
23
25
|
process.exit(1);
|
|
@@ -150,7 +152,7 @@ export async function runDeploy(options = {}) {
|
|
|
150
152
|
console.log(pc.dim(`Deployment ID: ${deploymentId}`));
|
|
151
153
|
|
|
152
154
|
while (true) {
|
|
153
|
-
const statusResult = await getDeployment(deploymentId);
|
|
155
|
+
const statusResult = await getDeployment(appId, deploymentId);
|
|
154
156
|
const current = statusResult?.data?.deployment;
|
|
155
157
|
if (!current) {
|
|
156
158
|
console.error(pc.red('Deployment not found while polling.'));
|
package/src/index.js
CHANGED
|
@@ -47,9 +47,10 @@ program
|
|
|
47
47
|
.description('Rsync local files and trigger a deployment')
|
|
48
48
|
.option('-m, --message <message>', 'Deployment message')
|
|
49
49
|
.option('--with-env', 'Upload local .env files (for React/Vite frontends)')
|
|
50
|
+
.option('-v, --verbose', 'Log API requests and responses')
|
|
50
51
|
.action(async (options) => {
|
|
51
52
|
try {
|
|
52
|
-
await runDeploy({ ...options, withEnv: Boolean(options.withEnv) });
|
|
53
|
+
await runDeploy({ ...options, withEnv: Boolean(options.withEnv), verbose: Boolean(options.verbose) });
|
|
53
54
|
} catch (err) {
|
|
54
55
|
console.error(pc.red(err.message || 'Deploy failed'));
|
|
55
56
|
process.exit(1);
|
package/src/lib/api.js
CHANGED
|
@@ -1,9 +1,134 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import pc from 'picocolors';
|
|
2
3
|
import { getApiBaseUrl, getToken } from './config.js';
|
|
3
4
|
|
|
5
|
+
let apiVerbose = false;
|
|
6
|
+
|
|
7
|
+
export function setApiVerbose(enabled) {
|
|
8
|
+
apiVerbose = Boolean(enabled);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isApiVerbose() {
|
|
12
|
+
return apiVerbose || process.env.SERVYX_DEBUG === '1' || process.env.SERVYX_DEBUG === 'true';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildFullUrl(baseURL = '', url = '') {
|
|
16
|
+
const base = String(baseURL).replace(/\/+$/, '');
|
|
17
|
+
const path = String(url).replace(/^\//, '');
|
|
18
|
+
return path ? `${base}/${path}` : base;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function summarizeBody(data) {
|
|
22
|
+
if (data == null) return '(empty)';
|
|
23
|
+
if (typeof data === 'string') {
|
|
24
|
+
const trimmed = data.trim();
|
|
25
|
+
if (!trimmed) return '(empty)';
|
|
26
|
+
if (trimmed.length > 800) {
|
|
27
|
+
return `${trimmed.slice(0, 800)}... [truncated, ${trimmed.length} chars]`;
|
|
28
|
+
}
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const text = JSON.stringify(data);
|
|
33
|
+
if (text.length > 1200) {
|
|
34
|
+
return `${text.slice(0, 1200)}... [truncated]`;
|
|
35
|
+
}
|
|
36
|
+
return text;
|
|
37
|
+
} catch {
|
|
38
|
+
return String(data);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function logApiRequest(config) {
|
|
43
|
+
const method = config.method?.toUpperCase() || 'REQUEST';
|
|
44
|
+
const url = config._servyxFullUrl || buildFullUrl(config.baseURL, config.url);
|
|
45
|
+
console.error(pc.dim(`→ ${method} ${url}`));
|
|
46
|
+
if (config.params && Object.keys(config.params).length) {
|
|
47
|
+
console.error(pc.dim(` Query: ${summarizeBody(config.params)}`));
|
|
48
|
+
}
|
|
49
|
+
if (config.data != null) {
|
|
50
|
+
console.error(pc.dim(` Body: ${summarizeBody(config.data)}`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function logApiResponse(response) {
|
|
55
|
+
const method = response.config?.method?.toUpperCase() || 'REQUEST';
|
|
56
|
+
const url = response.config?._servyxFullUrl || buildFullUrl(response.config?.baseURL, response.config?.url);
|
|
57
|
+
const elapsed = response.config?._servyxStart
|
|
58
|
+
? `${Date.now() - response.config._servyxStart}ms`
|
|
59
|
+
: '';
|
|
60
|
+
console.error(pc.dim(`← ${response.status} ${method} ${url}${elapsed ? ` (${elapsed})` : ''}`));
|
|
61
|
+
if (isApiVerbose()) {
|
|
62
|
+
console.error(pc.dim(` Response: ${summarizeBody(response.data)}`));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function logApiError({ method = 'REQUEST', url = 'unknown', err = null, response = null } = {}) {
|
|
67
|
+
console.error(pc.yellow('--- Servyx API error ---'));
|
|
68
|
+
console.error(pc.dim(`${method} ${url}`));
|
|
69
|
+
console.error(pc.dim(`API base: ${getApiBaseUrl()}`));
|
|
70
|
+
|
|
71
|
+
if (response) {
|
|
72
|
+
console.error(pc.dim(`Status: ${response.status} ${response.statusText || ''}`));
|
|
73
|
+
const headers = response.headers || {};
|
|
74
|
+
const contentType = headers['content-type'] || headers['Content-Type'];
|
|
75
|
+
if (contentType) {
|
|
76
|
+
console.error(pc.dim(`Content-Type: ${contentType}`));
|
|
77
|
+
}
|
|
78
|
+
console.error(pc.dim(`Response body: ${summarizeBody(response.data)}`));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (err) {
|
|
82
|
+
if (err.code) console.error(pc.dim(`Network code: ${err.code}`));
|
|
83
|
+
if (err.syscall) console.error(pc.dim(`Syscall: ${err.syscall}`));
|
|
84
|
+
if (err.errno != null) console.error(pc.dim(`Errno: ${err.errno}`));
|
|
85
|
+
if (err.address) console.error(pc.dim(`Address: ${err.address}${err.port ? `:${err.port}` : ''}`));
|
|
86
|
+
if (err.message) console.error(pc.dim(`Message: ${err.message}`));
|
|
87
|
+
if (err.config && !response) {
|
|
88
|
+
const reqUrl = err.config._servyxFullUrl || buildFullUrl(err.config.baseURL, err.config.url);
|
|
89
|
+
console.error(pc.dim(`Request: ${err.config.method?.toUpperCase() || 'REQUEST'} ${reqUrl}`));
|
|
90
|
+
if (err.config.data != null) {
|
|
91
|
+
console.error(pc.dim(`Request body: ${summarizeBody(err.config.data)}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.error(pc.yellow('------------------------'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function attachLogging(client) {
|
|
100
|
+
client.interceptors.request.use((config) => {
|
|
101
|
+
config._servyxStart = Date.now();
|
|
102
|
+
config._servyxFullUrl = buildFullUrl(config.baseURL, config.url);
|
|
103
|
+
if (isApiVerbose()) {
|
|
104
|
+
logApiRequest(config);
|
|
105
|
+
}
|
|
106
|
+
return config;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
client.interceptors.response.use(
|
|
110
|
+
(response) => {
|
|
111
|
+
if (isApiVerbose()) {
|
|
112
|
+
logApiResponse(response);
|
|
113
|
+
}
|
|
114
|
+
return response;
|
|
115
|
+
},
|
|
116
|
+
(error) => {
|
|
117
|
+
const config = error.config || {};
|
|
118
|
+
logApiError({
|
|
119
|
+
method: config.method?.toUpperCase() || 'REQUEST',
|
|
120
|
+
url: config._servyxFullUrl || buildFullUrl(config.baseURL, config.url),
|
|
121
|
+
err: error,
|
|
122
|
+
response: error.response || null,
|
|
123
|
+
});
|
|
124
|
+
return Promise.reject(error);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
4
129
|
function createClient(token = getToken()) {
|
|
5
130
|
const baseURL = `${getApiBaseUrl()}/api/v1/cli`;
|
|
6
|
-
|
|
131
|
+
const client = axios.create({
|
|
7
132
|
baseURL,
|
|
8
133
|
timeout: 120000,
|
|
9
134
|
headers: {
|
|
@@ -12,11 +137,20 @@ function createClient(token = getToken()) {
|
|
|
12
137
|
},
|
|
13
138
|
validateStatus: () => true,
|
|
14
139
|
});
|
|
140
|
+
attachLogging(client);
|
|
141
|
+
return client;
|
|
15
142
|
}
|
|
16
143
|
|
|
17
|
-
function mapNetworkError(err,
|
|
144
|
+
function mapNetworkError(err, meta = {}) {
|
|
18
145
|
if (err?.response || err?.status) throw err;
|
|
19
|
-
if (['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT'].includes(err?.code)) {
|
|
146
|
+
if (['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ECONNABORTED'].includes(err?.code)) {
|
|
147
|
+
if (!err?.config) {
|
|
148
|
+
logApiError({
|
|
149
|
+
method: meta.method || 'REQUEST',
|
|
150
|
+
url: meta.url || `${getApiBaseUrl()}/api/v1/cli${meta.path || ''}`,
|
|
151
|
+
err,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
20
154
|
const hint = new Error(
|
|
21
155
|
`Cannot reach Servyx API at ${getApiBaseUrl()} (${err.code}). Is cli-service running?`,
|
|
22
156
|
);
|
|
@@ -30,7 +164,17 @@ function unwrap(response) {
|
|
|
30
164
|
if (response.status >= 200 && response.status < 300) {
|
|
31
165
|
return response.data;
|
|
32
166
|
}
|
|
33
|
-
|
|
167
|
+
|
|
168
|
+
const method = response.config?.method?.toUpperCase() || 'REQUEST';
|
|
169
|
+
const url = response.config?._servyxFullUrl || buildFullUrl(response.config?.baseURL, response.config?.url);
|
|
170
|
+
logApiError({ method, url, response });
|
|
171
|
+
|
|
172
|
+
const isHtml = typeof response.data === 'string' && response.data.includes('<html');
|
|
173
|
+
const message = isHtml
|
|
174
|
+
? `API returned ${response.status} (expected JSON). Check SERVYX_API_URL / cli-service routing.`
|
|
175
|
+
: (response.data?.error || response.data?.message || response.statusText || 'Request failed');
|
|
176
|
+
|
|
177
|
+
const err = new Error(message);
|
|
34
178
|
err.status = response.status;
|
|
35
179
|
err.code = response.data?.code;
|
|
36
180
|
err.data = response.data;
|
|
@@ -40,6 +184,7 @@ function unwrap(response) {
|
|
|
40
184
|
export async function login({ username, password, otp, apiUrl }) {
|
|
41
185
|
const baseURL = `${(apiUrl || getApiBaseUrl()).replace(/\/+$/, '')}/api/v1/cli`;
|
|
42
186
|
const client = axios.create({ baseURL, timeout: 60000, validateStatus: () => true });
|
|
187
|
+
attachLogging(client);
|
|
43
188
|
const response = await client.post('/auth/login', { username, password, otp });
|
|
44
189
|
return unwrap(response);
|
|
45
190
|
}
|
|
@@ -55,20 +200,26 @@ export async function getDeployTarget(appId) {
|
|
|
55
200
|
}
|
|
56
201
|
|
|
57
202
|
export async function startDeployment(appId, message, { withEnv = false } = {}) {
|
|
203
|
+
const path = `/apps/${appId}/deployments`;
|
|
204
|
+
const body = {
|
|
205
|
+
message,
|
|
206
|
+
with_env: withEnv || undefined,
|
|
207
|
+
};
|
|
58
208
|
try {
|
|
59
209
|
const client = createClient();
|
|
60
|
-
return unwrap(await client.post(
|
|
61
|
-
message,
|
|
62
|
-
with_env: withEnv || undefined,
|
|
63
|
-
}));
|
|
210
|
+
return unwrap(await client.post(path, body));
|
|
64
211
|
} catch (err) {
|
|
65
|
-
mapNetworkError(err,
|
|
212
|
+
mapNetworkError(err, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
path,
|
|
215
|
+
url: buildFullUrl(`${getApiBaseUrl()}/api/v1/cli`, path),
|
|
216
|
+
});
|
|
66
217
|
}
|
|
67
218
|
}
|
|
68
219
|
|
|
69
|
-
export async function getDeployment(deploymentId) {
|
|
220
|
+
export async function getDeployment(appId, deploymentId) {
|
|
70
221
|
const client = createClient();
|
|
71
|
-
return unwrap(await client.get(`/deployments/${deploymentId}`));
|
|
222
|
+
return unwrap(await client.get(`/apps/${appId}/deployments/${deploymentId}`));
|
|
72
223
|
}
|
|
73
224
|
|
|
74
225
|
export async function listAppDeployments(appId, limit = 10) {
|