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 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.10",
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);
@@ -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
- 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
  );
@@ -30,7 +164,17 @@ function unwrap(response) {
30
164
  if (response.status >= 200 && response.status < 300) {
31
165
  return response.data;
32
166
  }
33
- const err = new Error(response.data?.error || response.statusText || 'Request failed');
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(`/apps/${appId}/deployments`, {
61
- message,
62
- with_env: withEnv || undefined,
63
- }));
210
+ return unwrap(await client.post(path, body));
64
211
  } catch (err) {
65
- mapNetworkError(err, 'start deployment');
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) {