servyx-cli 0.1.11 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "servyx-cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Servyx CLI — Upload local projects and deploy via the Servyx platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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);
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
- return axios.create({
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, action = 'request') {
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
  );
@@ -31,6 +165,10 @@ function unwrap(response) {
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
+
34
172
  const isHtml = typeof response.data === 'string' && response.data.includes('<html');
35
173
  const message = isHtml
36
174
  ? `API returned ${response.status} (expected JSON). Check SERVYX_API_URL / cli-service routing.`
@@ -46,6 +184,7 @@ function unwrap(response) {
46
184
  export async function login({ username, password, otp, apiUrl }) {
47
185
  const baseURL = `${(apiUrl || getApiBaseUrl()).replace(/\/+$/, '')}/api/v1/cli`;
48
186
  const client = axios.create({ baseURL, timeout: 60000, validateStatus: () => true });
187
+ attachLogging(client);
49
188
  const response = await client.post('/auth/login', { username, password, otp });
50
189
  return unwrap(response);
51
190
  }
@@ -61,14 +200,20 @@ export async function getDeployTarget(appId) {
61
200
  }
62
201
 
63
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
+ };
64
208
  try {
65
209
  const client = createClient();
66
- return unwrap(await client.post(`/apps/${appId}/deployments`, {
67
- message,
68
- with_env: withEnv || undefined,
69
- }));
210
+ return unwrap(await client.post(path, body));
70
211
  } catch (err) {
71
- mapNetworkError(err, 'start deployment');
212
+ mapNetworkError(err, {
213
+ method: 'POST',
214
+ path,
215
+ url: buildFullUrl(`${getApiBaseUrl()}/api/v1/cli`, path),
216
+ });
72
217
  }
73
218
  }
74
219