@boltic/cli 0.0.1

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.
@@ -0,0 +1,170 @@
1
+ import chalk from "chalk";
2
+ import open from "open";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import { getCliBearerToken, getCliSession } from "../api/login.js";
5
+ import { getCurrentEnv } from "../helper/env.js";
6
+ import { deleteAllSecrets, storeSecret } from "../helper/secure-storage.js";
7
+
8
+ // Define login commands and their actions
9
+ const commands = {
10
+ login: {
11
+ description: "Login to the platform and save access token",
12
+ action: handleLogin,
13
+ },
14
+ logout: {
15
+ description: "Logout and clear access token",
16
+ action: handleLogout,
17
+ },
18
+ help: { description: "Show help for login commands", action: showHelp },
19
+ };
20
+
21
+ // Execute a command
22
+ const execute = async (args) => {
23
+ const subCommand = args[0];
24
+
25
+ if (!subCommand || !commands[subCommand]) {
26
+ console.log(chalk.red("❌ Unknown or missing login sub-command.\n"));
27
+ showHelp();
28
+ return;
29
+ }
30
+
31
+ await commands[subCommand].action(args.slice(1));
32
+ };
33
+
34
+ // Show available login commands
35
+ function showHelp() {
36
+ console.log(chalk.cyan("\nLogin Commands:\n"));
37
+ Object.entries(commands).forEach(([cmd, details]) =>
38
+ console.log(chalk.bold(`${cmd}`) + ` - ${details.description}`)
39
+ );
40
+ }
41
+
42
+ // Handle login command
43
+ async function handleLogin(args) {
44
+ const { apiUrl, loginUrl, clientId, frontendUrl } = await getCurrentEnv();
45
+
46
+ const requestCode = uuidv4();
47
+ const state = {
48
+ source: "boltic_cli",
49
+ request_code: requestCode,
50
+ };
51
+
52
+ const loginPage = new URL(`${loginUrl}/auth/sign-in`);
53
+ loginPage.searchParams.append("client_id", clientId);
54
+ loginPage.searchParams.append("redirect_uri", frontendUrl);
55
+ loginPage.searchParams.append("state", JSON.stringify(state));
56
+
57
+ console.log(chalk.cyan("\n🌐 Opening browser for login..."));
58
+ try {
59
+ await open(loginPage.toString());
60
+ console.log(chalk.cyan("✅ Browser launched successfully"));
61
+ } catch (error) {
62
+ console.error(
63
+ chalk.red(
64
+ `\n❌ Failed to open browser automatically: ${error.message}`
65
+ )
66
+ );
67
+ console.log(
68
+ chalk.yellow("\n📋 Please copy and paste this URL in your browser:")
69
+ );
70
+ console.log(chalk.cyan("\n" + loginPage.toString() + "\n"));
71
+ }
72
+
73
+ const startTime = Date.now();
74
+ const timeout = 300000; // 5 minutes in milliseconds
75
+ const pollInterval = 5000; // 5 seconds
76
+
77
+ let lastProgressUpdate = 0;
78
+ console.log(chalk.cyan("\n⏳ Waiting for authentication..."));
79
+
80
+ while (Date.now() - startTime < timeout) {
81
+ try {
82
+ const sessionResponse = await getCliSession(apiUrl, requestCode);
83
+
84
+ if (!sessionResponse?.data?.data) {
85
+ const now = Date.now();
86
+ if (now - lastProgressUpdate >= pollInterval) {
87
+ process.stdout.write(chalk.yellow("."));
88
+ lastProgressUpdate = now;
89
+ }
90
+ continue;
91
+ }
92
+
93
+ const { account_id: accountId, session } =
94
+ sessionResponse.data.data;
95
+
96
+ if (!accountId || !session) {
97
+ console.log(
98
+ chalk.yellow(
99
+ "\n⚠️ Invalid session data received, retrying..."
100
+ )
101
+ );
102
+ continue;
103
+ }
104
+
105
+ try {
106
+ await storeSecret(
107
+ "session",
108
+ `bolt.session=${encodeURIComponent(session)}`
109
+ );
110
+ await storeSecret("account_id", accountId);
111
+
112
+ const token = await getCliBearerToken(
113
+ apiUrl,
114
+ accountId,
115
+ session
116
+ );
117
+
118
+ if (!token?.data?.data?.token) {
119
+ throw new Error("Invalid token response");
120
+ }
121
+
122
+ await storeSecret("token", token.data.data.token);
123
+ console.log(chalk.green("\n✅ Login successful!"));
124
+ return;
125
+ } catch (storageError) {
126
+ console.error(
127
+ chalk.red(
128
+ `\n❌ Failed to store authentication data: ${storageError.message}`
129
+ )
130
+ );
131
+ return;
132
+ }
133
+ } catch (error) {
134
+ if (error?.response?.status === 401) {
135
+ console.error(
136
+ chalk.red("\n\n❌ Authentication failed. Please try again.")
137
+ );
138
+ return;
139
+ } else if (error?.code === "ECONNREFUSED") {
140
+ console.error(
141
+ chalk.red("\n\n❌ Cannot connect to authentication server.")
142
+ );
143
+ return;
144
+ } else if (error?.response?.status !== 404) {
145
+ const now = Date.now();
146
+ if (now - lastProgressUpdate >= pollInterval) {
147
+ process.stdout.write(chalk.yellow("x"));
148
+ lastProgressUpdate = now;
149
+ }
150
+ }
151
+ }
152
+
153
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
154
+ }
155
+
156
+ console.error(
157
+ chalk.red("\n❌ Login timeout after 5 minutes. Please try again.")
158
+ );
159
+ }
160
+
161
+ // Handle logout command
162
+ async function handleLogout() {
163
+ await deleteAllSecrets();
164
+ console.log(
165
+ chalk.bgGreen.black("\n ✅ Success! ") +
166
+ chalk.green(" Logout successful! All user data cleared.\n")
167
+ );
168
+ }
169
+
170
+ export default { execute, handleLogin, handleLogout };
@@ -0,0 +1,13 @@
1
+ const environments = {
2
+ bolt: {
3
+ name: "boltic",
4
+ consoleUrl: "https://api.console.fynd.com",
5
+ apiUrl: "https://asia-south1.api.boltic.io",
6
+ loginUrl: "https://console.fynd.com",
7
+ clientId: "40ec7873-ce38-4f0a-923b-1ebe96887d78",
8
+ allowedOrigin: "*.console.boltic.io",
9
+ frontendUrl: "https://asia-south1.console.boltic.io",
10
+ },
11
+ };
12
+
13
+ export { environments };
@@ -0,0 +1,54 @@
1
+ // Helper function to calculate Levenshtein distance between two strings
2
+ function levenshteinDistance(str1, str2) {
3
+ const m = str1.length;
4
+ const n = str2.length;
5
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
6
+
7
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
8
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
9
+
10
+ for (let i = 1; i <= m; i++) {
11
+ for (let j = 1; j <= n; j++) {
12
+ if (str1[i - 1] === str2[j - 1]) {
13
+ dp[i][j] = dp[i - 1][j - 1];
14
+ } else {
15
+ dp[i][j] =
16
+ Math.min(
17
+ dp[i - 1][j - 1], // substitution
18
+ dp[i - 1][j], // deletion
19
+ dp[i][j - 1] // insertion
20
+ ) + 1;
21
+ }
22
+ }
23
+ }
24
+
25
+ return dp[m][n];
26
+ }
27
+
28
+ // Find similar commands based on Levenshtein distance and prefix matching
29
+ export function findSimilarCommands(invalidCommand, availableCommands) {
30
+ const threshold = 3; // Maximum distance to consider as similar
31
+ const suggestions = new Set(); // Use Set to avoid duplicates
32
+ const lowerInvalidCmd = invalidCommand.toLowerCase();
33
+
34
+ for (const cmd of Object.keys(availableCommands)) {
35
+ const lowerCmd = cmd.toLowerCase();
36
+
37
+ // Check for prefix match first
38
+ if (
39
+ lowerCmd.startsWith(lowerInvalidCmd) ||
40
+ lowerInvalidCmd.startsWith(lowerCmd)
41
+ ) {
42
+ suggestions.add(cmd);
43
+ continue;
44
+ }
45
+
46
+ // If no prefix match, check Levenshtein distance
47
+ const distance = levenshteinDistance(lowerInvalidCmd, lowerCmd);
48
+ if (distance <= threshold) {
49
+ suggestions.add(cmd);
50
+ }
51
+ }
52
+
53
+ return Array.from(suggestions);
54
+ }
package/helper/env.js ADDED
@@ -0,0 +1,27 @@
1
+ import { environments } from "../config/environments.js";
2
+ import { getAllSecrets } from "./secure-storage.js";
3
+
4
+ /**
5
+ * Get current environment and tokens from config
6
+ * @returns {Object} Environment configuration including apiUrl, token, session, and accountId
7
+ */
8
+ export const getCurrentEnv = async () => {
9
+ const secrets = await getAllSecrets();
10
+ let config;
11
+ if (secrets && secrets.length > 0) {
12
+ config = secrets.reduce((acc, { account, password }) => {
13
+ acc[account] = password;
14
+ return acc;
15
+ }, {});
16
+ }
17
+ return {
18
+ apiUrl: environments.bolt.apiUrl,
19
+ loginUrl: environments.bolt.loginUrl,
20
+ consoleUrl: environments.bolt.consoleUrl,
21
+ clientId: environments.bolt.clientId,
22
+ token: config?.token || null,
23
+ session: config?.session || null,
24
+ accountId: config?.account_id || null,
25
+ frontendUrl: environments.bolt.frontendUrl,
26
+ };
27
+ };
@@ -0,0 +1,123 @@
1
+ import chalk from "chalk";
2
+
3
+ // Error types for different scenarios
4
+ const ErrorType = {
5
+ API_ERROR: "API_ERROR",
6
+ NETWORK_ERROR: "NETWORK_ERROR",
7
+ AUTH_ERROR: "AUTH_ERROR",
8
+ VALIDATION_ERROR: "VALIDATION_ERROR",
9
+ CONFIG_ERROR: "CONFIG_ERROR",
10
+ UNKNOWN_ERROR: "UNKNOWN_ERROR",
11
+ };
12
+
13
+ // Format error message based on error type and response
14
+ const formatErrorMessage = (error) => {
15
+ if (!error)
16
+ return {
17
+ type: ErrorType.UNKNOWN_ERROR,
18
+ message: "An unknown error occurred",
19
+ };
20
+
21
+ // Handle API response errors
22
+ if (error.response) {
23
+ const { status, data } = error.response;
24
+
25
+ // Authentication errors
26
+ if (status === 401 || status === 403) {
27
+ return {
28
+ type: ErrorType.AUTH_ERROR,
29
+ message:
30
+ data.message ||
31
+ "Authentication failed. Please login again.",
32
+ };
33
+ }
34
+
35
+ // Validation errors
36
+ if (status === 400) {
37
+ return {
38
+ type: ErrorType.VALIDATION_ERROR,
39
+ message:
40
+ data.message || "Invalid request. Please check your input.",
41
+ };
42
+ }
43
+
44
+ // Server errors
45
+ if (status >= 500) {
46
+ return {
47
+ type: ErrorType.API_ERROR,
48
+ message:
49
+ error.response?.data?.error?.message ||
50
+ "Server error occurred. Please try again later.",
51
+ };
52
+ }
53
+
54
+ // Default API error
55
+ return {
56
+ type: ErrorType.API_ERROR,
57
+ message: data.message || `API Error: ${status}`,
58
+ };
59
+ }
60
+
61
+ // Network errors
62
+ if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
63
+ return {
64
+ type: ErrorType.NETWORK_ERROR,
65
+ message:
66
+ "Unable to connect to the server. Please check your internet connection.",
67
+ };
68
+ }
69
+
70
+ // Configuration errors
71
+ if (error.code === "ENOENT") {
72
+ return {
73
+ type: ErrorType.CONFIG_ERROR,
74
+ message: "Configuration file not found. Please run setup again.",
75
+ };
76
+ }
77
+
78
+ // Default unknown error
79
+ return {
80
+ type: ErrorType.UNKNOWN_ERROR,
81
+ message: error.message || "An unexpected error occurred",
82
+ };
83
+ };
84
+
85
+ // Display formatted error message to user
86
+ const handleError = (error) => {
87
+ const formattedError = formatErrorMessage(error);
88
+
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);
119
+ }
120
+ process.exit(1);
121
+ };
122
+
123
+ export { ErrorType, handleError };
@@ -0,0 +1,204 @@
1
+ import chalk from "chalk";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import {
5
+ authentication,
6
+ base,
7
+ resource1,
8
+ webhook,
9
+ } from "../templates/schemas.js";
10
+
11
+ export const createIntegrationFolderStructure = async (integration) => {
12
+ const { id, name, description, icon, activity_type, trigger_type, meta } =
13
+ integration;
14
+
15
+ const spec = {
16
+ id,
17
+ name,
18
+ description,
19
+ icon,
20
+ activity_type,
21
+ trigger_type,
22
+ meta,
23
+ };
24
+ // Create integration folder structure
25
+ const integrationName = name.toLowerCase().replace(/\s+/g, "-");
26
+ const integrationDir = path.join(process.cwd(), integrationName);
27
+
28
+ // Ensure the integration directory doesn't exist
29
+ if (fs.existsSync(integrationDir)) {
30
+ console.log(
31
+ chalk.yellow(
32
+ `\nWarning: Directory ${integrationDir} already exists!`
33
+ )
34
+ );
35
+ return integrationDir;
36
+ }
37
+
38
+ // Create main directory
39
+ fs.mkdirSync(integrationDir, { recursive: true });
40
+
41
+ // Create schemas directory and its subdirectories
42
+ const schemasDir = path.join(integrationDir, "schemas");
43
+ const resourcesDir = path.join(schemasDir, "resources");
44
+ fs.mkdirSync(resourcesDir, { recursive: true });
45
+
46
+ // Create template files
47
+ const files = {
48
+ "schemas/resources/resource1.json": JSON.stringify(resource1, null, 4),
49
+ "schemas/authentication.json": JSON.stringify(authentication, null, 4),
50
+ "schemas/base.json": JSON.stringify(base(name), null, 4),
51
+ "schemas/webhook.json": JSON.stringify(webhook(name), null, 4),
52
+ "spec.json": JSON.stringify(spec, null, 4),
53
+ "Authentication.mdx": `# ${name} Authentication
54
+
55
+ Describe the authentication process for ${name} integration here.`,
56
+ "Documentation.mdx": `# ${name} Documentation
57
+
58
+ ${description}
59
+
60
+ ## Overview
61
+
62
+ Add integration overview here.
63
+
64
+ ## Features
65
+
66
+ - Feature 1
67
+ - Feature 2
68
+ - Feature 3
69
+
70
+ ## Setup Guide
71
+
72
+ 1. Step 1
73
+ 2. Step 2
74
+ 3. Step 3`,
75
+ };
76
+
77
+ // Create all files
78
+ for (const [filePath, content] of Object.entries(files)) {
79
+ const fullPath = path.join(integrationDir, filePath);
80
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
81
+ fs.writeFileSync(fullPath, content);
82
+ }
83
+
84
+ console.log(chalk.cyan(`To navigate to your integration folder, run:\n`));
85
+ console.log(chalk.white(` cd ${integrationName}\n`));
86
+ };
87
+
88
+ export const createExistingIntegrationsFolder = async (payload) => {
89
+ const {
90
+ integration,
91
+ authentication,
92
+ webhook,
93
+ configuration,
94
+ resources,
95
+ operations,
96
+ } = payload;
97
+
98
+ const {
99
+ id,
100
+ name,
101
+ description,
102
+ icon,
103
+ activity_type,
104
+ trigger_type,
105
+ documentation,
106
+ meta,
107
+ } = integration;
108
+
109
+ const spec = {
110
+ id,
111
+ name,
112
+ description,
113
+ icon,
114
+ activity_type,
115
+ trigger_type,
116
+ meta,
117
+ };
118
+
119
+ const integrationName = name.toLowerCase().replace(/\s+/g, "-");
120
+ const integrationDir = path.join(process.cwd(), integrationName);
121
+
122
+ // Log if the directory already exists
123
+ if (fs.existsSync(integrationDir)) {
124
+ console.log(
125
+ chalk.yellow(
126
+ `\nNotice: Directory ${integrationDir} already exists. Updating contents...\n`
127
+ )
128
+ );
129
+ } else {
130
+ console.log(
131
+ chalk.green(`\nCreating integration directory: ${integrationDir}\n`)
132
+ );
133
+ }
134
+
135
+ // Ensure all necessary folders exist
136
+ fs.mkdirSync(integrationDir, { recursive: true });
137
+
138
+ const schemasDir = path.join(integrationDir, "schemas");
139
+ const resourcesDir = path.join(schemasDir, "resources");
140
+ fs.mkdirSync(resourcesDir, { recursive: true });
141
+
142
+ const authentication_documentation = authentication.documentation;
143
+
144
+ // Define files and content
145
+ const files = {
146
+ "schemas/authentication.json": JSON.stringify(
147
+ authentication.content || {},
148
+ null,
149
+ 4
150
+ ),
151
+ "schemas/base.json": JSON.stringify(
152
+ configuration?.content || {},
153
+ null,
154
+ 4
155
+ ),
156
+ "schemas/webhook.json": JSON.stringify(webhook?.content || {}, null, 4),
157
+ "spec.json": JSON.stringify(spec, null, 4),
158
+ "Authentication.mdx": authentication_documentation || "",
159
+ "Documentation.mdx": documentation || "",
160
+ };
161
+
162
+ // Write resource files
163
+ resources.forEach((resource) => {
164
+ const resource_id = resource.id;
165
+ const resourceName = resource.name.toLowerCase().replace(/\s+/g, "-");
166
+ const resourcePath = path.join(resourcesDir, `${resourceName}.json`);
167
+
168
+ const resourceOperations = operations.filter(
169
+ (operation) => operation.resource_id === resource_id
170
+ );
171
+
172
+ const operationsContent = resourceOperations.reduce(
173
+ (acc, operation) => {
174
+ const operationName = operation.name
175
+ .toLowerCase()
176
+ .replace(/\s+/g, "-");
177
+ acc[operationName] = operation.content;
178
+ return acc;
179
+ },
180
+ {}
181
+ );
182
+
183
+ const resourceFileContent = {
184
+ ...resource.content,
185
+ ...operationsContent,
186
+ };
187
+
188
+ fs.writeFileSync(
189
+ resourcePath,
190
+ JSON.stringify(resourceFileContent, null, 4)
191
+ );
192
+ });
193
+
194
+ // Write or overwrite all defined files
195
+ for (const [filePath, content] of Object.entries(files)) {
196
+ const fullPath = path.join(integrationDir, filePath);
197
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
198
+ fs.writeFileSync(fullPath, content);
199
+ }
200
+
201
+ console.log(chalk.cyan(`\nIntegration folder is ready at:`));
202
+ console.log(chalk.white(` ${integrationDir}\n`));
203
+ return true;
204
+ };
@@ -0,0 +1,74 @@
1
+ import keytar from "keytar";
2
+
3
+ const SERVICE_NAME = "boltic-cli";
4
+
5
+ /**
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>}
10
+ */
11
+ export const storeSecret = async (key, value) => {
12
+ try {
13
+ await keytar.setPassword(SERVICE_NAME, key, value);
14
+ } catch (error) {
15
+ console.error(`Error storing secret for ${key}:`, error.message);
16
+ throw error;
17
+ }
18
+ };
19
+
20
+ /**
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
24
+ */
25
+ export const getSecret = async (key) => {
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;
31
+ }
32
+ };
33
+
34
+ /**
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
38
+ */
39
+ export const deleteSecret = async (key) => {
40
+ try {
41
+ return await keytar.deletePassword(SERVICE_NAME, key);
42
+ } catch (error) {
43
+ console.error(`Error deleting secret for ${key}:`, error.message);
44
+ return false;
45
+ }
46
+ };
47
+
48
+ /**
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
51
+ */
52
+
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;
59
+ }
60
+ };
61
+
62
+ export const deleteAllSecrets = async () => {
63
+ try {
64
+ const secrets = await getAllSecrets();
65
+ if (secrets && secrets.length > 0) {
66
+ const deletionPromises = secrets.map(
67
+ async ({ account }) => await deleteSecret(account)
68
+ );
69
+ await Promise.all(deletionPromises);
70
+ }
71
+ } catch (error) {
72
+ console.error(`Error deleting all secrets:`, error.message);
73
+ }
74
+ };
@@ -0,0 +1,20 @@
1
+ import chalk from "chalk";
2
+
3
+ let isVerbose = false;
4
+
5
+ export const setVerboseMode = (verbose) => {
6
+ isVerbose = verbose;
7
+ };
8
+
9
+ export const getVerboseMode = () => {
10
+ return isVerbose;
11
+ };
12
+
13
+ export const logApi = (method, url, status) => {
14
+ if (!isVerbose) return;
15
+ console.log(
16
+ chalk(
17
+ `https fetch ${chalk.cyan(method.toUpperCase())} ${chalk.green(status)} ${chalk.yellow(url)}`
18
+ )
19
+ );
20
+ };
package/index.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ import createCLI from "./cli.js";
4
+ import { environments } from "./config/environments.js";
5
+
6
+ // Get environment-specific URLs
7
+ const env = environments.bolt;
8
+ const FYND_CONSOLE_URL = env.consoleUrl;
9
+ const BOLTIC_API_URL = env.apiUrl;
10
+
11
+ (async () => {
12
+ const cli = createCLI(FYND_CONSOLE_URL, BOLTIC_API_URL, env);
13
+ await cli.execute(process.argv);
14
+ })();