@boltic/cli 1.0.44-dev1.3 → 1.0.44

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.
@@ -3,7 +3,8 @@ import FormData from "form-data";
3
3
  import fs from "fs";
4
4
  import https from "https";
5
5
  import { handleError } from "../helper/error.js";
6
- import { logApi } from "../helper/verbose.js";
6
+ import { getSecret } from "../helper/secure-storage.js";
7
+ import { logApi, logApiRequest, logApiResponse } from "../helper/verbose.js";
7
8
 
8
9
  const getHttpsAgentForUrl = (baseUrl) => {
9
10
  try {
@@ -22,20 +23,33 @@ const getHttpsAgentForUrl = (baseUrl) => {
22
23
  return undefined;
23
24
  };
24
25
 
25
- // When session is absent the token is a PAT → use x-boltic-token header.
26
- // When session is present the token is a bearer token → use Authorization + Cookie.
27
- const buildAuthHeaders = (token, session) => {
26
+ const buildAuthHeaders = async (token, session) => {
27
+ const pat = await getSecret("pat");
28
+
29
+ // If PAT exists, prefer PAT-based auth and do not send bearer/session
30
+ if (pat && pat.trim()) {
31
+ return {
32
+ "x-boltic-token": pat.trim(),
33
+ };
34
+ }
35
+
36
+ // Fallback to existing Bearer + Cookie auth
37
+ const headers = {};
38
+ if (token) {
39
+ headers.Authorization = `Bearer ${token}`;
40
+ }
28
41
  if (session) {
29
- const headers = {};
30
- if (token) headers.Authorization = `Bearer ${token}`;
31
42
  headers.Cookie = session;
32
- return headers;
33
43
  }
34
- return token ? { "x-boltic-token": token } : {};
44
+ return headers;
35
45
  };
36
46
 
37
- const ensureAuthenticatedOrExit = (accountId, token) => {
38
- if (!token || !accountId) {
47
+ const ensureAuthenticatedOrExit = async (accountId, token, session) => {
48
+ const pat = await getSecret("pat");
49
+ const hasPatAuth = pat && pat.trim() && accountId;
50
+ const hasSessionAuth = token && session && accountId;
51
+
52
+ if (!hasPatAuth && !hasSessionAuth) {
39
53
  console.error(
40
54
  "\x1b[31mError:\x1b[0m Authentication credentials are required."
41
55
  );
@@ -47,8 +61,8 @@ const ensureAuthenticatedOrExit = (accountId, token) => {
47
61
 
48
62
  const getIntegrationGroups = async (apiUrl, accountId, token, session) => {
49
63
  try {
50
- ensureAuthenticatedOrExit(accountId, token);
51
- const authHeaders = buildAuthHeaders(token, session);
64
+ await ensureAuthenticatedOrExit(accountId, token, session);
65
+ const authHeaders = await buildAuthHeaders(token, session);
52
66
  const axiosOptions = {
53
67
  method: "get",
54
68
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integration-groups`,
@@ -73,8 +87,8 @@ const getIntegrationGroups = async (apiUrl, accountId, token, session) => {
73
87
 
74
88
  const listAllIntegrations = async (apiUrl, token, accountId, session) => {
75
89
  try {
76
- ensureAuthenticatedOrExit(accountId, token);
77
- const authHeaders = buildAuthHeaders(token, session);
90
+ await ensureAuthenticatedOrExit(accountId, token, session);
91
+ const authHeaders = await buildAuthHeaders(token, session);
78
92
  const axiosOptions = {
79
93
  method: "get",
80
94
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations`,
@@ -105,8 +119,8 @@ const saveIntegration = async (
105
119
  integration
106
120
  ) => {
107
121
  try {
108
- ensureAuthenticatedOrExit(accountId, token);
109
- const authHeaders = buildAuthHeaders(token, session);
122
+ await ensureAuthenticatedOrExit(accountId, token, session);
123
+ const authHeaders = await buildAuthHeaders(token, session);
110
124
  const response = await axios({
111
125
  method: "post",
112
126
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations`,
@@ -125,12 +139,14 @@ const saveIntegration = async (
125
139
 
126
140
  const editIntegration = async (apiUrl, token, accountId, session, payload) => {
127
141
  const { id } = payload;
142
+ const url = `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${id}/edit`;
128
143
  try {
129
- ensureAuthenticatedOrExit(accountId, token);
130
- const authHeaders = buildAuthHeaders(token, session);
144
+ await ensureAuthenticatedOrExit(accountId, token, session);
145
+ const authHeaders = await buildAuthHeaders(token, session);
146
+ logApiRequest("post", url, payload);
131
147
  const response = await axios({
132
148
  method: "post",
133
- url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${id}/edit`,
149
+ url,
134
150
  data: payload,
135
151
  headers: {
136
152
  "Content-Type": "application/json",
@@ -138,9 +154,10 @@ const editIntegration = async (apiUrl, token, accountId, session, payload) => {
138
154
  },
139
155
  httpsAgent: getHttpsAgentForUrl(apiUrl),
140
156
  });
141
-
157
+ logApiResponse(response.status, response.data);
142
158
  return response.data.data;
143
159
  } catch (error) {
160
+ logApiResponse(error.response?.status, error.response?.data);
144
161
  handleError(error);
145
162
  }
146
163
  };
@@ -153,8 +170,8 @@ const updateIntegration = async (
153
170
  integration
154
171
  ) => {
155
172
  try {
156
- ensureAuthenticatedOrExit(accountId, token);
157
- const authHeaders = buildAuthHeaders(token, session);
173
+ await ensureAuthenticatedOrExit(accountId, token, session);
174
+ const authHeaders = await buildAuthHeaders(token, session);
158
175
  const { id, ...rest } = integration;
159
176
  const response = await axios({
160
177
  method: "patch",
@@ -180,8 +197,8 @@ const getIntegrationById = async (
180
197
  integrationId
181
198
  ) => {
182
199
  try {
183
- ensureAuthenticatedOrExit(accountId, token);
184
- const authHeaders = buildAuthHeaders(token, session);
200
+ await ensureAuthenticatedOrExit(accountId, token, session);
201
+ const authHeaders = await buildAuthHeaders(token, session);
185
202
  const response = await axios({
186
203
  method: "get",
187
204
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${integrationId}`,
@@ -205,8 +222,8 @@ const getAuthenticationByIntegrationId = async (
205
222
  integrationId
206
223
  ) => {
207
224
  try {
208
- ensureAuthenticatedOrExit(accountId, token);
209
- const authHeaders = buildAuthHeaders(token, session);
225
+ await ensureAuthenticatedOrExit(accountId, token, session);
226
+ const authHeaders = await buildAuthHeaders(token, session);
210
227
  const response = await axios({
211
228
  method: "get",
212
229
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}integrations/${integrationId}/authentication`,
@@ -229,7 +246,14 @@ const getWebhooksByIntegrationId = async (
229
246
  session,
230
247
  integrationId
231
248
  ) => {
232
- ensureAuthenticatedOrExit(accountId, token);
249
+ if (!token || !session || !accountId) {
250
+ console.error(
251
+ "\x1b[31mError:\x1b[0m Authentication credentials are required."
252
+ );
253
+ console.log("\n🔹 Please log in first using:");
254
+ console.log("\x1b[32m$ boltic login\x1b[0m\n");
255
+ process.exit(1); // Exit the CLI with an error code
256
+ }
233
257
 
234
258
  try {
235
259
  const response = await axios({
@@ -237,7 +261,8 @@ const getWebhooksByIntegrationId = async (
237
261
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}integrations/${integrationId}/webhooks`,
238
262
  headers: {
239
263
  "Content-Type": "application/json",
240
- ...buildAuthHeaders(token, session),
264
+ Authorization: `Bearer ${token}`,
265
+ Cookie: session,
241
266
  },
242
267
  httpsAgent: getHttpsAgentForUrl(apiUrl),
243
268
  });
@@ -255,8 +280,8 @@ const getConfigurationByIntegrationId = async (
255
280
  integrationId
256
281
  ) => {
257
282
  try {
258
- ensureAuthenticatedOrExit(accountId, token);
259
- const authHeaders = buildAuthHeaders(token, session);
283
+ await ensureAuthenticatedOrExit(accountId, token, session);
284
+ const authHeaders = await buildAuthHeaders(token, session);
260
285
  const response = await axios({
261
286
  method: "get",
262
287
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}integrations/${integrationId}/configuration`,
@@ -280,8 +305,8 @@ const syncIntegration = async (
280
305
  integration
281
306
  ) => {
282
307
  try {
283
- ensureAuthenticatedOrExit(accountId, token);
284
- const authHeaders = buildAuthHeaders(token, session);
308
+ await ensureAuthenticatedOrExit(accountId, token, session);
309
+ const authHeaders = await buildAuthHeaders(token, session);
285
310
  const response = await axios({
286
311
  method: "post",
287
312
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${integration.integration_id}/deploy`,
@@ -306,14 +331,15 @@ const sendIntegrationForReview = async (
306
331
  integration
307
332
  ) => {
308
333
  try {
309
- ensureAuthenticatedOrExit(accountId, token);
334
+ await ensureAuthenticatedOrExit(accountId, token, session);
310
335
  const response = await axios({
311
336
  method: "post",
312
337
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integration-reviews`,
313
338
  data: integration,
314
339
  headers: {
315
340
  "Content-Type": "application/json",
316
- ...buildAuthHeaders(token, session),
341
+ Authorization: `Bearer ${token}`,
342
+ Cookie: session,
317
343
  },
318
344
  httpsAgent: getHttpsAgentForUrl(apiUrl),
319
345
  });
@@ -326,8 +352,8 @@ const sendIntegrationForReview = async (
326
352
  const purgeCache = async (apiUrl, token, accountId, session, integration) => {
327
353
  const { integration_id } = integration;
328
354
  try {
329
- ensureAuthenticatedOrExit(accountId, token);
330
- const authHeaders = buildAuthHeaders(token, session);
355
+ await ensureAuthenticatedOrExit(accountId, token, session);
356
+ const authHeaders = await buildAuthHeaders(token, session);
331
357
  const response = await axios({
332
358
  method: "post",
333
359
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${integration_id}/cache`,
@@ -346,8 +372,8 @@ const purgeCache = async (apiUrl, token, accountId, session, integration) => {
346
372
 
347
373
  const pullIntegration = async (apiUrl, token, accountId, session, id) => {
348
374
  try {
349
- ensureAuthenticatedOrExit(accountId, token);
350
- const authHeaders = buildAuthHeaders(token, session);
375
+ await ensureAuthenticatedOrExit(accountId, token, session);
376
+ const authHeaders = await buildAuthHeaders(token, session);
351
377
  const response = await axios({
352
378
  method: "get",
353
379
  url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${id}/pull`,
@@ -376,8 +402,8 @@ const uploadFileToCloud = async (
376
402
  }
377
403
 
378
404
  try {
379
- ensureAuthenticatedOrExit(accountId, token);
380
- const authHeaders = buildAuthHeaders(token, session);
405
+ await ensureAuthenticatedOrExit(accountId, token, session);
406
+ const authHeaders = await buildAuthHeaders(token, session);
381
407
  const form = new FormData();
382
408
  form.append("files", fs.createReadStream(filePath));
383
409
 
package/api/serverless.js CHANGED
@@ -22,31 +22,6 @@ const getHttpsAgentForUrl = (baseUrl) => {
22
22
  return undefined;
23
23
  };
24
24
 
25
- // When session is absent the token is a PAT → use x-boltic-token header.
26
- // When session is present the token is a bearer token → use Authorization + Cookie.
27
- const buildAuthHeaders = (token, session) => {
28
- if (session) {
29
- const headers = {};
30
- if (token) headers.Authorization = `Bearer ${token}`;
31
- headers.Cookie = session;
32
- return headers;
33
- }
34
- return token ? { "x-boltic-token": token } : {};
35
- };
36
-
37
- // Returns true when the user has sufficient credentials for any auth method.
38
- // Pass accountId when the endpoint requires it; omit (undefined) when it doesn't.
39
- // Passing null means "required but missing" → returns false.
40
- const isAuthenticated = (token, session, accountId) => {
41
- if (accountId !== undefined) {
42
- // PAT auth: token + accountId (no session needed)
43
- // Bearer auth: token + session + accountId
44
- return !!(token && accountId);
45
- }
46
- // For endpoints that don't need accountId: PAT needs just token, bearer needs token+session
47
- return !!token;
48
- };
49
-
50
25
  const listAllServerless = async (
51
26
  apiUrl,
52
27
  token,
@@ -54,7 +29,7 @@ const listAllServerless = async (
54
29
  session,
55
30
  query = null
56
31
  ) => {
57
- if (!isAuthenticated(token, session, accountId)) {
32
+ if (!token || !session || !accountId) {
58
33
  console.error(
59
34
  "\x1b[31mError:\x1b[0m Authentication credentials are required."
60
35
  );
@@ -81,7 +56,8 @@ const listAllServerless = async (
81
56
  params,
82
57
  headers: {
83
58
  "Content-Type": "application/json",
84
- ...buildAuthHeaders(token, session),
59
+ Authorization: `Bearer ${token}`,
60
+ Cookie: session,
85
61
  },
86
62
  httpsAgent: getHttpsAgentForUrl(apiUrl),
87
63
  };
@@ -99,7 +75,7 @@ const listAllServerless = async (
99
75
  };
100
76
 
101
77
  const pullServerless = async (apiUrl, token, accountId, session, id) => {
102
- if (!isAuthenticated(token, session, accountId)) {
78
+ if (!token || !session || !accountId) {
103
79
  console.error(
104
80
  "\x1b[31mError:\x1b[0m Authentication credentials are required."
105
81
  );
@@ -115,7 +91,8 @@ const pullServerless = async (apiUrl, token, accountId, session, id) => {
115
91
  url,
116
92
  headers: {
117
93
  "Content-Type": "application/json",
118
- ...buildAuthHeaders(token, session),
94
+ Authorization: `Bearer ${token}`,
95
+ Cookie: session,
119
96
  },
120
97
  httpsAgent: getHttpsAgentForUrl(apiUrl),
121
98
  });
@@ -127,7 +104,7 @@ const pullServerless = async (apiUrl, token, accountId, session, id) => {
127
104
  };
128
105
 
129
106
  const publishServerless = async (apiUrl, token, session, payload) => {
130
- if (!isAuthenticated(token, session)) {
107
+ if (!token || !session) {
131
108
  console.error(
132
109
  "\x1b[31mError:\x1b[0m Authentication credentials are required."
133
110
  );
@@ -142,7 +119,8 @@ const publishServerless = async (apiUrl, token, session, payload) => {
142
119
  url: `${apiUrl}/service/panel/serverless/v1.0/apps`,
143
120
  headers: {
144
121
  "Content-Type": "application/json",
145
- ...buildAuthHeaders(token, session),
122
+ Authorization: `Bearer ${token}`,
123
+ Cookie: session,
146
124
  },
147
125
  data: payload,
148
126
  httpsAgent: getHttpsAgentForUrl(apiUrl),
@@ -164,7 +142,7 @@ const updateServerless = async (
164
142
  serverlessId,
165
143
  payload
166
144
  ) => {
167
- if (!isAuthenticated(token, session)) {
145
+ if (!token || !session) {
168
146
  console.error(
169
147
  "\x1b[31mError:\x1b[0m Authentication credentials are required."
170
148
  );
@@ -179,7 +157,8 @@ const updateServerless = async (
179
157
  url: `${apiUrl}/service/panel/serverless/v1.0/apps/${serverlessId}`,
180
158
  headers: {
181
159
  "Content-Type": "application/json",
182
- ...buildAuthHeaders(token, session),
160
+ Authorization: `Bearer ${token}`,
161
+ Cookie: session,
183
162
  },
184
163
  data: payload,
185
164
  httpsAgent: getHttpsAgentForUrl(apiUrl),
@@ -202,7 +181,7 @@ const getServerlessBuilds = async (
202
181
  serverlessId,
203
182
  options = {}
204
183
  ) => {
205
- if (!isAuthenticated(token, session, accountId)) {
184
+ if (!token || !session || !accountId) {
206
185
  console.error(
207
186
  "\x1b[31mError:\x1b[0m Authentication credentials are required."
208
187
  );
@@ -222,7 +201,8 @@ const getServerlessBuilds = async (
222
201
  },
223
202
  headers: {
224
203
  "Content-Type": "application/json",
225
- ...buildAuthHeaders(token, session),
204
+ Authorization: `Bearer ${token}`,
205
+ Cookie: session,
226
206
  },
227
207
  httpsAgent: getHttpsAgentForUrl(apiUrl),
228
208
  };
@@ -243,7 +223,7 @@ const getServerlessLogs = async (
243
223
  serverlessId,
244
224
  options = {}
245
225
  ) => {
246
- if (!isAuthenticated(token, session, accountId)) {
226
+ if (!token || !session || !accountId) {
247
227
  console.error(
248
228
  "\x1b[31mError:\x1b[0m Authentication credentials are required."
249
229
  );
@@ -272,7 +252,8 @@ const getServerlessLogs = async (
272
252
  params,
273
253
  headers: {
274
254
  "Content-Type": "application/json",
275
- ...buildAuthHeaders(token, session),
255
+ Authorization: `Bearer ${token}`,
256
+ Cookie: session,
276
257
  },
277
258
  httpsAgent: getHttpsAgentForUrl(apiUrl),
278
259
  };
@@ -293,7 +274,7 @@ const getBuildLogs = async (
293
274
  serverlessId,
294
275
  buildId
295
276
  ) => {
296
- if (!isAuthenticated(token, session, accountId)) {
277
+ if (!token || !session || !accountId) {
297
278
  console.error(
298
279
  "\x1b[31mError:\x1b[0m Authentication credentials are required."
299
280
  );
@@ -313,7 +294,8 @@ const getBuildLogs = async (
313
294
  },
314
295
  headers: {
315
296
  "Content-Type": "application/json",
316
- ...buildAuthHeaders(token, session),
297
+ Authorization: `Bearer ${token}`,
298
+ Cookie: session,
317
299
  },
318
300
  httpsAgent: getHttpsAgentForUrl(apiUrl),
319
301
  };
package/commands/login.js CHANGED
@@ -234,17 +234,17 @@ async function handlePatLogin(patFromArg, accountIdFromArg) {
234
234
  // If both values are provided via CLI flags, do not prompt at all.
235
235
  if (pat && accountId) {
236
236
  try {
237
- await storeSecret("token", pat);
237
+ await storeSecret("pat", pat);
238
238
  await storeSecret("account_id", accountId);
239
239
  console.log(
240
240
  chalk.green(
241
- "\n✅ Token and Account ID stored securely. They will be used for future organization-related requests.\n"
241
+ "\n✅ PAT token and Account ID stored securely. They will be used for future organization-related requests.\n"
242
242
  )
243
243
  );
244
244
  } catch (error) {
245
245
  console.error(
246
246
  chalk.red(
247
- `\n❌ Failed to store credentials: ${error.message || error}\n`
247
+ `\n❌ Failed to store PAT credentials: ${error.message || error}\n`
248
248
  )
249
249
  );
250
250
  }
@@ -271,7 +271,7 @@ async function handlePatLogin(patFromArg, accountIdFromArg) {
271
271
  }
272
272
 
273
273
  try {
274
- await storeSecret("token", pat);
274
+ await storeSecret("pat", pat);
275
275
  await storeSecret("account_id", accountId);
276
276
  console.log(
277
277
  chalk.green(
@@ -267,7 +267,7 @@ async function handleCreate(args = []) {
267
267
  */
268
268
  async function checkServerlessExists(name) {
269
269
  const env = await getCurrentEnv();
270
- if (!env || !env.token) {
270
+ if (!env || !env.token || !env.session) {
271
271
  return null; // Can't check without auth, let the create call handle auth error
272
272
  }
273
273
 
@@ -379,7 +379,7 @@ async function handleCodeTypeCreate(
379
379
 
380
380
  // Get authentication credentials
381
381
  const env = await getCurrentEnv();
382
- if (!env || !env.token) {
382
+ if (!env || !env.token || !env.session) {
383
383
  console.error(chalk.red("\n❌ Not authenticated. Please login first."));
384
384
  console.log(chalk.yellow(" Run: boltic login"));
385
385
  return;
@@ -523,7 +523,7 @@ async function handleGitTypeCreate(
523
523
 
524
524
  // Get authentication credentials first
525
525
  const env = await getCurrentEnv();
526
- if (!env || !env.token) {
526
+ if (!env || !env.token || !env.session) {
527
527
  console.error(chalk.red("\n❌ Not authenticated. Please login first."));
528
528
  console.log(chalk.yellow(" Run: boltic login"));
529
529
  // Cleanup the created directory
package/helper/error.js CHANGED
@@ -6,10 +6,49 @@ const ErrorType = {
6
6
  NETWORK_ERROR: "NETWORK_ERROR",
7
7
  AUTH_ERROR: "AUTH_ERROR",
8
8
  VALIDATION_ERROR: "VALIDATION_ERROR",
9
+ NOT_FOUND_ERROR: "NOT_FOUND_ERROR",
9
10
  CONFIG_ERROR: "CONFIG_ERROR",
10
11
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
11
12
  };
12
13
 
14
+ /**
15
+ * Extract the most meaningful error message from an API response body.
16
+ * Checks every known backend format before giving up.
17
+ */
18
+ const extractApiMessage = (data) => {
19
+ if (!data) return null;
20
+ if (typeof data === "string") return data;
21
+
22
+ // Backend standard: { error: { message } }
23
+ if (data.error?.message) return data.error.message;
24
+
25
+ // Flat: { message }
26
+ if (typeof data.message === "string") return data.message;
27
+
28
+ // Validation array in meta: { error: { meta: { errors: [...] } } }
29
+ if (data.error?.meta?.errors?.length) {
30
+ return data.error.meta.errors
31
+ .map((e) =>
32
+ typeof e === "string" ? e : e.message || JSON.stringify(e)
33
+ )
34
+ .join("; ");
35
+ }
36
+
37
+ // Top-level errors array: { errors: [...] }
38
+ if (Array.isArray(data.errors) && data.errors.length) {
39
+ return data.errors
40
+ .map((e) =>
41
+ typeof e === "string" ? e : e.message || JSON.stringify(e)
42
+ )
43
+ .join("; ");
44
+ }
45
+
46
+ // Fallback: { detail } (some frameworks)
47
+ if (typeof data.detail === "string") return data.detail;
48
+
49
+ return null;
50
+ };
51
+
13
52
  // Format error message based on error type and response
14
53
  const formatErrorMessage = (error) => {
15
54
  if (!error)
@@ -21,14 +60,14 @@ const formatErrorMessage = (error) => {
21
60
  // Handle API response errors
22
61
  if (error.response) {
23
62
  const { status, data } = error.response;
63
+ const apiMessage = extractApiMessage(data);
24
64
 
25
65
  // Authentication errors
26
66
  if (status === 401 || status === 403) {
27
67
  return {
28
68
  type: ErrorType.AUTH_ERROR,
29
69
  message:
30
- data.message ||
31
- "Authentication failed. Please login again.",
70
+ apiMessage || "Authentication failed. Please login again.",
32
71
  };
33
72
  }
34
73
 
@@ -37,7 +76,25 @@ const formatErrorMessage = (error) => {
37
76
  return {
38
77
  type: ErrorType.VALIDATION_ERROR,
39
78
  message:
40
- data.message || "Invalid request. Please check your input.",
79
+ apiMessage || "Invalid request. Please check your input.",
80
+ };
81
+ }
82
+
83
+ // Not found errors
84
+ if (status === 404) {
85
+ return {
86
+ type: ErrorType.NOT_FOUND_ERROR,
87
+ message: apiMessage || "The requested resource was not found.",
88
+ };
89
+ }
90
+
91
+ // Conflict errors
92
+ if (status === 409) {
93
+ return {
94
+ type: ErrorType.API_ERROR,
95
+ message:
96
+ apiMessage ||
97
+ "A conflict occurred with the current state of the resource.",
41
98
  };
42
99
  }
43
100
 
@@ -46,7 +103,7 @@ const formatErrorMessage = (error) => {
46
103
  return {
47
104
  type: ErrorType.API_ERROR,
48
105
  message:
49
- error.response?.data?.error?.message ||
106
+ apiMessage ||
50
107
  "Server error occurred. Please try again later.",
51
108
  };
52
109
  }
@@ -54,7 +111,7 @@ const formatErrorMessage = (error) => {
54
111
  // Default API error
55
112
  return {
56
113
  type: ErrorType.API_ERROR,
57
- message: data.message || `API Error: ${status}`,
114
+ message: apiMessage || `API Error: ${status}`,
58
115
  };
59
116
  }
60
117
 
@@ -67,6 +124,14 @@ const formatErrorMessage = (error) => {
67
124
  };
68
125
  }
69
126
 
127
+ // Timeout errors
128
+ if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
129
+ return {
130
+ type: ErrorType.NETWORK_ERROR,
131
+ message: "Request timed out. Please try again.",
132
+ };
133
+ }
134
+
70
135
  // Configuration errors
71
136
  if (error.code === "ENOENT") {
72
137
  return {
@@ -86,37 +151,32 @@ const formatErrorMessage = (error) => {
86
151
  const handleError = (error) => {
87
152
  const formattedError = formatErrorMessage(error);
88
153
 
89
- switch (formattedError.type) {
90
- case ErrorType.AUTH_ERROR:
91
- console.error(
92
- chalk.red("\n❌ Authentication Error:"),
93
- formattedError.message
94
- );
95
- break;
96
- case ErrorType.API_ERROR:
97
- console.error(chalk.red("\n❌ API Error:"), formattedError.message);
98
- break;
99
- case ErrorType.NETWORK_ERROR:
100
- console.error(
101
- chalk.red("\n❌ Network Error:"),
102
- formattedError.message
103
- );
104
- break;
105
- case ErrorType.VALIDATION_ERROR:
106
- console.error(
107
- chalk.red("\n❌ Validation Error:"),
108
- formattedError.message
109
- );
110
- break;
111
- case ErrorType.CONFIG_ERROR:
112
- console.error(
113
- chalk.red("\n❌ Configuration Error:"),
114
- formattedError.message
115
- );
116
- break;
117
- default:
118
- console.error(chalk.red("\n❌ Error:"), formattedError.message);
154
+ const labels = {
155
+ [ErrorType.AUTH_ERROR]: "Authentication Error",
156
+ [ErrorType.API_ERROR]: "API Error",
157
+ [ErrorType.NETWORK_ERROR]: "Network Error",
158
+ [ErrorType.VALIDATION_ERROR]: "Validation Error",
159
+ [ErrorType.NOT_FOUND_ERROR]: "Not Found",
160
+ [ErrorType.CONFIG_ERROR]: "Configuration Error",
161
+ [ErrorType.UNKNOWN_ERROR]: "Error",
162
+ };
163
+
164
+ const label = labels[formattedError.type] || "Error";
165
+ console.error(chalk.red(`\n❌ ${label}:`), formattedError.message);
166
+
167
+ // Always show API response details for debugging when there is a response
168
+ if (error?.response) {
169
+ const { status, data, config } = error.response;
170
+ console.error(
171
+ chalk.gray(
172
+ `\n ${config?.method?.toUpperCase() || "?"} ${config?.url || "?"} → ${status}`
173
+ )
174
+ );
175
+ if (data) {
176
+ console.error(chalk.gray(` Response: ${JSON.stringify(data)}`));
177
+ }
119
178
  }
179
+
120
180
  process.exit(1);
121
181
  };
122
182
 
@@ -1,86 +1,42 @@
1
- const SERVICE_NAME = "boltic-cli";
2
-
3
- // Mapping from credential keys to environment variable names.
4
- // In CI/headless environments where keytar (OS keychain) is unavailable,
5
- // these env vars are used as a fallback so commands like `boltic serverless publish`
6
- // work without an interactive keychain daemon.
7
- const ENV_VAR_MAP = {
8
- token: "BOLTIC_TOKEN",
9
- account_id: "BOLTIC_ACCOUNT_ID",
10
- session: "BOLTIC_SESSION",
11
- environment: "BOLTIC_ENVIRONMENT",
12
- };
1
+ import keytar from "keytar";
13
2
 
14
- // Lazy-load keytar via dynamic import so that a missing native dependency
15
- // (e.g. libsecret on Linux CI runners) is caught at call time rather than
16
- // crashing the process at startup with ERR_DLOPEN_FAILED.
17
- // The result is cached after the first attempt.
18
- let _keytar;
19
- let _keytarAttempted = false;
20
-
21
- const getKeytar = async () => {
22
- if (_keytarAttempted) return _keytar;
23
- _keytarAttempted = true;
24
- try {
25
- _keytar = (await import("keytar")).default;
26
- } catch {
27
- // Native library not available (e.g. libsecret missing on CI runners).
28
- _keytar = null;
29
- }
30
- return _keytar;
31
- };
3
+ const SERVICE_NAME = "boltic-cli";
32
4
 
33
5
  /**
34
- * Store a secret value securely using keytar.
35
- * In CI/headless environments where keytar is unavailable, logs a warning
36
- * instead of throwing so that env-var-based auth can still be used.
6
+ * Store a secret value securely using keytar
7
+ * @param {string} key - The key under which to store the secret
8
+ * @param {string} value - The secret value to store
9
+ * @returns {Promise<void>}
37
10
  */
38
11
  export const storeSecret = async (key, value) => {
39
- const keytar = await getKeytar();
40
- if (!keytar) {
41
- console.warn(
42
- `Warning: System keychain is unavailable. Could not store '${key}'.`
43
- );
44
- console.warn(
45
- `In CI environments, set credentials via environment variables (e.g. BOLTIC_TOKEN, BOLTIC_ACCOUNT_ID).`
46
- );
47
- return;
48
- }
49
12
  try {
50
13
  await keytar.setPassword(SERVICE_NAME, key, value);
51
14
  } catch (error) {
52
- console.warn(
53
- `Warning: Could not store '${key}' in system keychain: ${error.message}`
54
- );
55
- console.warn(
56
- `In CI environments, set credentials via environment variables (e.g. BOLTIC_TOKEN, BOLTIC_ACCOUNT_ID).`
57
- );
15
+ console.error(`Error storing secret for ${key}:`, error.message);
16
+ throw error;
58
17
  }
59
18
  };
60
19
 
61
20
  /**
62
- * Retrieve a secret value. Tries keytar first; falls back to env vars
63
- * (BOLTIC_TOKEN, BOLTIC_ACCOUNT_ID, etc.) when keytar is unavailable.
21
+ * Retrieve a secret value using keytar
22
+ * @param {string} key - The key of the secret to retrieve
23
+ * @returns {Promise<string|null>} The secret value or null if not found
64
24
  */
65
25
  export const getSecret = async (key) => {
66
- const keytar = await getKeytar();
67
- if (keytar) {
68
- try {
69
- const val = await keytar.getPassword(SERVICE_NAME, key);
70
- if (val !== null) return val;
71
- } catch {
72
- // keytar failed — fall through to env var
73
- }
26
+ try {
27
+ return await keytar.getPassword(SERVICE_NAME, key);
28
+ } catch (error) {
29
+ console.error(`Error retrieving secret for ${key}:`, error.message);
30
+ return null;
74
31
  }
75
- return process.env[ENV_VAR_MAP[key]] || null;
76
32
  };
77
33
 
78
34
  /**
79
- * Delete a secret value using keytar.
35
+ * Delete a secret value using keytar
36
+ * @param {string} key - The key of the secret to delete
37
+ * @returns {Promise<boolean>} True if deletion was successful
80
38
  */
81
39
  export const deleteSecret = async (key) => {
82
- const keytar = await getKeytar();
83
- if (!keytar) return false;
84
40
  try {
85
41
  return await keytar.deletePassword(SERVICE_NAME, key);
86
42
  } catch (error) {
@@ -90,28 +46,17 @@ export const deleteSecret = async (key) => {
90
46
  };
91
47
 
92
48
  /**
93
- * Retrieve all secrets. Tries keytar first; falls back to env vars when
94
- * keytar is unavailable (e.g. GitHub Actions, Docker containers).
49
+ * Retrieve all secrets stored using keytar
50
+ * @returns {Promise<Array<{account: string, password: string}>|null>} An array of secret objects or null if an error occurs
95
51
  */
96
- export const getAllSecrets = async () => {
97
- const keytar = await getKeytar();
98
- if (keytar) {
99
- try {
100
- const keytarSecrets = await keytar.findCredentials(SERVICE_NAME);
101
- if (keytarSecrets && keytarSecrets.length > 0) return keytarSecrets;
102
- } catch {
103
- // keytar failed — fall through to env vars
104
- }
105
- }
106
52
 
107
- // Build credential list from env vars
108
- const secrets = [];
109
- for (const [key, envVar] of Object.entries(ENV_VAR_MAP)) {
110
- if (process.env[envVar]) {
111
- secrets.push({ account: key, password: process.env[envVar] });
112
- }
53
+ export const getAllSecrets = async () => {
54
+ try {
55
+ return await keytar.findCredentials(SERVICE_NAME);
56
+ } catch (error) {
57
+ console.error(`Error retrieving all secrets:`, error.message);
58
+ return null;
113
59
  }
114
- return secrets.length > 0 ? secrets : null;
115
60
  };
116
61
 
117
62
  export const deleteAllSecrets = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boltic/cli",
3
- "version": "1.0.44-dev1.3",
3
+ "version": "1.0.44",
4
4
  "description": "Professional CLI for interacting with the Boltic platform — create, manage, and publish integrations, serverless functions, workflows, MCPs, and more with enterprise-grade features and a seamless developer experience",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -40,6 +40,7 @@
40
40
  "scripts": {
41
41
  "start": "node index.js",
42
42
  "dev": "nodemon index.js",
43
+ "dev:debug": "nodemon --inspect index.js",
43
44
  "test": "jest",
44
45
  "test:watch": "jest --watch",
45
46
  "test:coverage": "jest --coverage",