@docupace/pipeline 1.0.39-newrelease.8 → 1.0.39

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.
@@ -1,437 +0,0 @@
1
- #!/usr/bin/env node
2
- import dotenv from 'dotenv';
3
- dotenv.config({ path: ['.env.pipeline.local', 'config/.env.pipeline.local'] });
4
-
5
- import fs from 'fs';
6
- import path from 'path';
7
- import fetch from 'node-fetch';
8
- import yaml from 'js-yaml';
9
- import { z } from 'zod';
10
-
11
- // Configuration
12
- export const JIRA_BASE_URL =
13
- process.env.JIRA_BASE_URL || 'https://docupacellc.atlassian.net';
14
-
15
- const JIRA_API_KEY = process.env.JIRA_API_KEY;
16
- const JIRA_API_EMAIL = process.env.JIRA_API_EMAIL;
17
- const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
18
- const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-4.1';
19
-
20
- const CHANGESET_DIR = path.join(process.cwd(), '.release-notes');
21
-
22
- // Define the Zod schema for OpenAI response
23
- const openAiResponseSchema = z.object({
24
- title: z
25
- .string()
26
- .describe('A concise title for the release note (3-8 words)'),
27
- summary: z.string().describe('A brief user-facing summary of changes'),
28
- technicalDescription: z
29
- .string()
30
- .optional()
31
- .describe('Developer-friendly description of technical changes'),
32
- userFacingDescription: z
33
- .string()
34
- .optional()
35
- .describe('Plain-language description of changes from a user perspective'),
36
- technicalOnly: z
37
- .boolean()
38
- .describe(
39
- 'Whether this change is technical-only with no user-visible impact',
40
- ),
41
- changeSize: z
42
- .enum(['Small', 'Medium', 'Large'])
43
- .describe('Size/scope of the change'),
44
- changeType: z.enum(['New', 'Improved', 'Fixed']).describe('Type of change'),
45
- });
46
-
47
- /**
48
- * Log token usage information
49
- * @param {string} ticketId - The JIRA ticket ID
50
- * @param {Object} tokenUsage - Token usage data
51
- */
52
- /**
53
- * Strip sensitive or unnecessary fields from a JIRA ticket object
54
- * @param {Object} ticketData - The JIRA ticket data to clean
55
- * @returns {Object} - The cleaned JIRA ticket data
56
- */
57
- function stripJiraTicketFields(ticketData) {
58
- if (!ticketData || !ticketData.fields) return ticketData;
59
-
60
- const fieldsToStrip = [
61
- 'customfield_10020',
62
- 'worklog',
63
- 'creator',
64
- 'reporter',
65
- 'assignee',
66
- 'customfield_10095',
67
- 'customfield_10000',
68
- 'votes',
69
- 'watches',
70
- 'customfield_10083',
71
- ];
72
-
73
- const cleanedData = JSON.parse(JSON.stringify(ticketData)); // Create a deep copy
74
-
75
- // Strip specified fields
76
- fieldsToStrip.forEach((field) => {
77
- if (cleanedData.fields[field]) {
78
- delete cleanedData.fields[field];
79
- }
80
- });
81
-
82
- // Remove all null fields
83
- Object.keys(cleanedData.fields).forEach((field) => {
84
- if (cleanedData.fields[field] === null) {
85
- delete cleanedData.fields[field];
86
- }
87
- });
88
-
89
- // Clean up comments if they exist
90
- if (cleanedData.fields.comment && cleanedData.fields.comment.comments) {
91
- cleanedData.fields.comment.comments =
92
- cleanedData.fields.comment.comments.map((comment) => {
93
- const cleanComment = { ...comment };
94
- if (cleanComment.author) delete cleanComment.author;
95
- if (cleanComment.updateAuthor) delete cleanComment.updateAuthor;
96
-
97
- // Remove null fields from comments
98
- Object.keys(cleanComment).forEach((key) => {
99
- if (cleanComment[key] === null) {
100
- delete cleanComment[key];
101
- }
102
- });
103
-
104
- return cleanComment;
105
- });
106
- }
107
-
108
- return cleanedData;
109
- }
110
-
111
- /**
112
- * Fetch JIRA ticket information
113
- * @param {string} ticketId - The JIRA ticket ID
114
- * @returns {Promise<Object>} - The JIRA ticket data
115
- */
116
- async function fetchJiraTicket(ticketId) {
117
- if (!JIRA_API_KEY || !JIRA_API_EMAIL) {
118
- console.warn('JIRA credentials not provided. Skipping JIRA API call.');
119
- return null;
120
- }
121
-
122
- const auth = Buffer.from(`${JIRA_API_EMAIL}:${JIRA_API_KEY}`).toString(
123
- 'base64',
124
- );
125
-
126
- try {
127
- const response = await fetch(
128
- `${JIRA_BASE_URL}/rest/api/3/issue/${ticketId}`,
129
- {
130
- headers: {
131
- Authorization: `Basic ${auth}`,
132
- 'Content-Type': 'application/json',
133
- },
134
- },
135
- );
136
-
137
- if (!response.ok) {
138
- console.error(
139
- `Error fetching JIRA ticket ${ticketId}: ${response.statusText}`,
140
- );
141
- return null;
142
- }
143
-
144
- const data = await response.json();
145
- const cleanedData = stripJiraTicketFields(data);
146
-
147
- // Check if the ticket is a subtask
148
- const isSubtask = cleanedData.fields.issuetype?.subtask || false;
149
- let mainTicketId, subTaskTicketId, mainTicketData, subTaskTicketData;
150
-
151
- // If it's a subtask, fetch the parent ticket as well
152
- if (isSubtask && cleanedData.fields.parent) {
153
- const parentTicketId = cleanedData.fields.parent.key;
154
- mainTicketId = parentTicketId;
155
- subTaskTicketId = ticketId;
156
- delete cleanedData.fields.parent;
157
-
158
- console.log(
159
- `Fetching parent ticket ${parentTicketId} for subtask ${ticketId}`,
160
- );
161
-
162
- const parentResponse = await fetch(
163
- `${JIRA_BASE_URL}/rest/api/3/issue/${parentTicketId}`,
164
- {
165
- headers: {
166
- Authorization: `Basic ${auth}`,
167
- 'Content-Type': 'application/json',
168
- },
169
- },
170
- );
171
-
172
- if (parentResponse.ok) {
173
- const parentData = await parentResponse.json();
174
- const cleanedParentData = stripJiraTicketFields(parentData);
175
-
176
- mainTicketData = cleanedParentData;
177
- subTaskTicketData = cleanedData;
178
- }
179
- } else {
180
- mainTicketId = ticketId;
181
- mainTicketData = cleanedData;
182
- }
183
-
184
- return {
185
- mainTicketData,
186
- subTaskTicketData,
187
- mainTicketId,
188
- subTaskTicketId,
189
- };
190
- } catch (error) {
191
- console.error(`Error fetching JIRA ticket ${ticketId}:`, error.message);
192
- return null;
193
- }
194
- }
195
-
196
- /**
197
- * Generate enhanced release notes using OpenAI
198
- * @param {Object} ticketData - The JIRA ticket data
199
- * @returns {Promise<Object>} - Enhanced release notes
200
- */
201
- export async function generateEnhancedReleaseNotes(ticketData) {
202
- if (!OPENAI_API_KEY) {
203
- console.warn(
204
- 'OpenAI API key not provided. Skipping release notes enhancement.',
205
- );
206
- return null;
207
- }
208
-
209
- if (!ticketData) return null;
210
-
211
- try {
212
- const systemPrompt = `Process the provided JSON from a Jira ticket to generate structured release notes in JSON format.
213
- Follow this strict schema to generate your response:
214
-
215
- {
216
- "title": "A concise title for the release note (3-8 words)",
217
- "summary": "A brief user-facing summary of changes (max 2 sentences)",
218
- "technicalDescription": "Developer-friendly description of technical changes in markdown format (max 5 sentences)",
219
- "userFacingDescription": "Provide only if changes made to user interface. Plain-language description of changes from a user perspective in markdown format (max 5 sentences)",
220
- "technicalOnly": boolean (true/false whether this change is only technical with no user-visible impact),
221
- "changeType": "New" | "Improved" | "Fixed",
222
- "changeSize": "Small" | "Medium" | "Large"
223
- }
224
-
225
- When creating your response:
226
- - Title: Create a concise title summarizing the change (3–8 words). Avoid repeating the Jira ticket title.
227
- - Summary: Write a maximum two-sentence summary in user-facing language.
228
- - Technical Description: Use developer-friendly language to explain what changed at the system or code level. Include only if specified: API impacts, architectural changes, or deployment considerations. Format using markdown. Use backticks for code (\`like_this\`), bold for important terms, and line breaks for readability. Avoid headings like Technical Changes
229
- - User-Facing Description: Provide only if changes are made to the user interface. Use plain language to explain what users will see or benefit from. Include affected UI elements, workflows, or systems. Prefer bullet points if listing multiple effects or changes. Use markdown formatting for clarity.
230
- - Technical Only: Set to true if the change has no visible effect to end users. Examples for technical only are API changes, refactoring of code or architectual changes that do not have changes to the user interface
231
- - Change Type: Classify as “New”, “Improved”, or “Fixed”.
232
- - Change Size: Estimate the impact as “Small”, “Medium”, or “Large”.
233
-
234
- For Descriptions:
235
- Avoid large blocks of text. Break content into short paragraphs or lists. Use markdown consistently: code, bold, italic, and single level bullet lists (- Item). Make your response easy to scan, especially the descriptions. Focus on what changed, how it works, and where it applies.
236
- Avoid restating the ticket title. Focus on what changed, how it works, and where it applies.
237
- `;
238
-
239
- const userPrompt = JSON.stringify(ticketData, null, 2);
240
- const ticketId =
241
- ticketData.mainTicketId || ticketData.mainTicketData?.key || 'unknown';
242
-
243
- const response = await fetch('https://api.openai.com/v1/chat/completions', {
244
- method: 'POST',
245
- headers: {
246
- Authorization: `Bearer ${OPENAI_API_KEY}`,
247
- 'Content-Type': 'application/json',
248
- },
249
- body: JSON.stringify({
250
- model: OPENAI_MODEL,
251
- messages: [
252
- { role: 'system', content: systemPrompt },
253
- { role: 'user', content: userPrompt },
254
- ],
255
- response_format: {
256
- type: 'json_schema',
257
- json_schema: {
258
- name: 'release_notes',
259
- schema: {
260
- type: 'object',
261
- properties: {
262
- title: {
263
- type: 'string',
264
- description:
265
- 'A concise title for the release note (3-8 words)',
266
- },
267
- summary: {
268
- type: 'string',
269
- description:
270
- 'A brief user-facing summary of changes (max 2 sentences)',
271
- },
272
- technicalDescription: {
273
- type: 'string',
274
- description:
275
- 'Developer-friendly description of technical changes (max 5 sentences)',
276
- },
277
- userFacingDescription: {
278
- type: 'string',
279
- description:
280
- 'Plain-language description of changes from a user perspective (max 5 sentences)',
281
- },
282
- technicalOnly: {
283
- type: 'boolean',
284
- description:
285
- 'Whether this change is technical-only with no user-visible impact',
286
- },
287
- changeSize: {
288
- type: 'string',
289
- enum: ['Small', 'Medium', 'Large'],
290
- description: 'Size/scope of the change',
291
- },
292
- changeType: {
293
- type: 'string',
294
- enum: ['New', 'Improved', 'Fixed'],
295
- description: 'Type of change',
296
- },
297
- },
298
- required: [
299
- 'title',
300
- 'summary',
301
- 'technicalOnly',
302
- 'changeSize',
303
- 'changeType',
304
- ],
305
- additionalProperties: false,
306
- },
307
- },
308
- },
309
- temperature: 0, // Lower temperature for more structured/predictable responses
310
- }),
311
- });
312
-
313
- if (!response.ok) {
314
- console.error(`Error from OpenAI API: ${response.statusText}`);
315
- return null;
316
- }
317
-
318
- const data = await response.json();
319
-
320
- const content = JSON.parse(data.choices[0].message.content);
321
-
322
- // Validate response using Zod schema
323
- const validatedResponse = openAiResponseSchema.parse(content);
324
-
325
- return validatedResponse;
326
- } catch (error) {
327
- console.error('Error generating enhanced release notes:', error.message);
328
- return null;
329
- }
330
- }
331
-
332
- export async function writeYamlFile(mainTicketId, enhancedNotes) {
333
- // Create changeset directory if it doesn't exist
334
- if (!fs.existsSync(path.join(process.cwd(), '.release-notes'))) {
335
- fs.mkdirSync(path.join(process.cwd(), '.release-notes'));
336
- }
337
-
338
- const yamlFileName = `${mainTicketId}.yaml`;
339
- const yamlFilePath = path.join(CHANGESET_DIR, yamlFileName);
340
-
341
- const yamlContent = yaml.dump(
342
- {
343
- ticketId: mainTicketId,
344
- title: enhancedNotes.title,
345
- summary: enhancedNotes.summary,
346
- technicalDescription: {
347
- __line: enhancedNotes.technicalDescription,
348
- },
349
- userFacingDescription: {
350
- __line: enhancedNotes.userFacingDescription,
351
- },
352
- technicalOnly: enhancedNotes.technicalOnly,
353
- changeSize: enhancedNotes.changeSize,
354
- changeType: enhancedNotes.changeType,
355
- },
356
- {
357
- lineWidth: -1, // Don't wrap lines
358
- noRefs: true,
359
- schema: yaml.DEFAULT_SCHEMA.extend({
360
- implicit: [
361
- new yaml.Type('tag:yaml.org,2002:str', {
362
- kind: 'scalar',
363
- instanceOf: Object,
364
- resolve: function (object) {
365
- return object !== null && object.hasOwnProperty('__line');
366
- },
367
- construct: function (object) {
368
- return object.__line;
369
- },
370
- predicate: function (object) {
371
- return object.hasOwnProperty('__line');
372
- },
373
- represent: function (object) {
374
- return { __line: object.__line };
375
- },
376
- }),
377
- ],
378
- }),
379
- },
380
- );
381
-
382
- // Replace the problematic YAML output with properly formatted literal block scalars
383
- const fixedYaml = yamlContent
384
- .replace(/technicalDescription: '?(.*?)'?$/m, (match, p1) => {
385
- return `technicalDescription: |\n ${enhancedNotes.technicalDescription.replace(/\n/g, '\n ')}`;
386
- })
387
- .replace(/userFacingDescription: '?(.*?)'?$/m, (match, p1) => {
388
- return `userFacingDescription: |\n ${enhancedNotes.userFacingDescription.replace(/\n/g, '\n ')}`;
389
- });
390
-
391
- fs.writeFileSync(yamlFilePath, fixedYaml, 'utf8');
392
- }
393
-
394
- export async function getReleaseNotes(ticketId) {
395
- const ticketData = await fetchJiraTicket(ticketId);
396
-
397
- if (ticketData) {
398
- // Generate enhanced release notes
399
- const { mainTicketData, subTaskTicketData, mainTicketId } = ticketData;
400
-
401
- const yamlFileName = `${mainTicketId}.yaml`;
402
- const yamlFilePath = path.join(CHANGESET_DIR, yamlFileName);
403
-
404
- if (fs.existsSync(yamlFilePath)) {
405
- console.log(
406
- `Notes for ticket ${mainTicketId} already exist, loading from file`,
407
- );
408
- try {
409
- const fileContent = fs.readFileSync(yamlFilePath, 'utf8');
410
- const enhancedNotes = yaml.load(fileContent);
411
- return {
412
- yamlFilePath,
413
- enhancedNotes,
414
- mainTicketId: enhancedNotes.ticketId,
415
- };
416
- } catch (error) {
417
- console.error(
418
- `Error loading YAML file for ${mainTicketId}:`,
419
- error.message,
420
- );
421
- }
422
- }
423
-
424
- const enhancedNotes = await generateEnhancedReleaseNotes({
425
- mainTicketData,
426
- subTaskTicketData,
427
- mainTicketId,
428
- });
429
-
430
- if (enhancedNotes) {
431
- await writeYamlFile(mainTicketId, enhancedNotes);
432
- return { yamlFilePath, enhancedNotes, mainTicketId };
433
- } else {
434
- throw new Error(`Cannot generate release notes for ${mainTicketId}`);
435
- }
436
- }
437
- }
@@ -1,86 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
-
4
- export function getPackageNames(rootPath = '.') {
5
- const appsDir = path.join(rootPath, 'apps');
6
- const packagesDir = path.join(rootPath, 'packages');
7
- const rootPackageJson = path.join(rootPath, 'package.json');
8
-
9
- // Check if this is a monorepo (has apps and/or packages directories)
10
- const hasAppsDir =
11
- fs.existsSync(appsDir) && fs.statSync(appsDir).isDirectory();
12
- const hasPackagesDir =
13
- fs.existsSync(packagesDir) && fs.statSync(packagesDir).isDirectory();
14
-
15
- if (hasAppsDir || hasPackagesDir) {
16
- // This is a monorepo
17
- const packages = [];
18
-
19
- // Get packages from apps directory
20
- if (hasAppsDir) {
21
- const appsPackages = getPackagesFromDirectory(appsDir);
22
- packages.push(...appsPackages);
23
- }
24
-
25
- // Get packages from packages directory
26
- if (hasPackagesDir) {
27
- const packagesPackages = getPackagesFromDirectory(packagesDir);
28
- packages.push(...packagesPackages);
29
- }
30
-
31
- return packages;
32
- } else {
33
- // This is not a monorepo, return single package
34
- if (fs.existsSync(rootPackageJson)) {
35
- try {
36
- const packageJson = JSON.parse(
37
- fs.readFileSync(rootPackageJson, 'utf8'),
38
- );
39
- return packageJson.name
40
- ? [{ name: packageJson.name, path: rootPath }]
41
- : [];
42
- } catch (error) {
43
- console.error('Error reading root package.json:', error);
44
- return [];
45
- }
46
- }
47
- return [];
48
- }
49
- }
50
-
51
- function getPackagesFromDirectory(dirPath) {
52
- const packages = [];
53
-
54
- try {
55
- const items = fs.readdirSync(dirPath);
56
-
57
- for (const item of items) {
58
- const itemPath = path.join(dirPath, item);
59
- const packageJsonPath = path.join(itemPath, 'package.json');
60
-
61
- // Check if item is a directory and has package.json
62
- if (
63
- fs.statSync(itemPath).isDirectory() &&
64
- fs.existsSync(packageJsonPath)
65
- ) {
66
- try {
67
- const packageJson = JSON.parse(
68
- fs.readFileSync(packageJsonPath, 'utf8'),
69
- );
70
- if (packageJson.name) {
71
- packages.push({
72
- name: packageJson.name,
73
- path: itemPath,
74
- });
75
- }
76
- } catch (error) {
77
- console.error(`Error reading package.json in ${itemPath}:`, error);
78
- }
79
- }
80
- }
81
- } catch (error) {
82
- console.error(`Error reading directory ${dirPath}:`, error);
83
- }
84
-
85
- return packages;
86
- }
@@ -1,43 +0,0 @@
1
- import path from 'path';
2
- import fs from 'fs';
3
-
4
- async function fileExists(filePath) {
5
- try {
6
- await fs.promises.access(filePath);
7
- return true;
8
- } catch {
9
- return false;
10
- }
11
- }
12
- export async function getMainPackage() {
13
- const mainModulePath = process.cwd();
14
- return getPackage(mainModulePath);
15
- }
16
-
17
- export async function getPackage(mainModulePath) {
18
- if (mainModulePath) {
19
- const packageJsonPath = path.join(mainModulePath, 'package.json');
20
- if (await fileExists(packageJsonPath)) {
21
- const fileContent = await fs.promises.readFile(packageJsonPath, 'utf-8');
22
- const packageJson = JSON.parse(fileContent);
23
- return packageJson;
24
- }
25
- }
26
- throw new Error('Main package.json not found');
27
- }
28
-
29
- /**
30
- * Locates and extracts a JIRA Ticket ID from a given string.
31
- * <p>
32
- * The JIRA Ticket ID is expected to follow the pattern of uppercase letters (at least two) followed by a hyphen and digits (e.g., DC-1234, INFP-9).
33
- * @param str the string to search for a JIRA ticket ID
34
- * @returns {JIRA-TicketID|null}
35
- */
36
- export function findJiraTicketId(str) {
37
- if (!str) {
38
- return null;
39
- }
40
- const regex = /([A-Z]{2,}-\d+)/;
41
- const matches = str.match(regex);
42
- return matches ? matches[0] : null;
43
- }
@@ -1,102 +0,0 @@
1
- import { setGitInfo } from '../next/util.js';
2
- import { $ } from 'zx';
3
- import fs from 'fs';
4
- import { updateComponentVersion } from '../next/jira.js';
5
- import { findJiraTicketId } from './util.js';
6
-
7
- const changeSetPackage = `{
8
- "name": "changeset-package",
9
- "version": "1.0.0",
10
- "description": "",
11
- "scripts": {
12
- },
13
- "type": "module",
14
- "keywords": [],
15
- "author": "",
16
- "license": "ISC",
17
- "dependencies": {},
18
- "devDependencies": {
19
- "@changesets/cli": "^2.27.10",
20
- "@docupace/pipeline": "1.0.39-newrelease.7"
21
- }
22
- }
23
- `;
24
-
25
- export async function loadChangesetOutput() {
26
- const outputPath = './changeset-output.json';
27
- const jsonOutput = JSON.parse(await fs.promises.readFile(outputPath, 'utf8'));
28
- return jsonOutput;
29
- }
30
-
31
- async function removeChangesetOutput() {
32
- const outputPath = './changeset-output.json';
33
- await fs.promises.rm(outputPath);
34
- }
35
-
36
- export async function versionStatus(sinceBranch) {
37
- const outputPath = './changeset-output.json';
38
-
39
- if (sinceBranch) {
40
- await $`pnpm changeset status --output=${outputPath} --since=${sinceBranch}`;
41
- } else {
42
- await $`pnpm changeset status --output=${outputPath}`;
43
- }
44
-
45
- // Read the file and parse JSON
46
- const jsonOutput = JSON.parse(await fs.promises.readFile(outputPath, 'utf8'));
47
-
48
- return jsonOutput.releases;
49
- }
50
-
51
- export async function versionCommand(closeVersion = false) {
52
- console.log('📦 Running Changeset versioning...');
53
- await setGitInfo();
54
- await $`mv package.json package.json.bak`;
55
- await $`mv pnpm-lock.yaml pnpm-lock.yaml.bak`;
56
- await fs.promises.writeFile('package.json', changeSetPackage);
57
- await $`pnpm install`;
58
- await $`rm package.json pnpm-lock.yaml`;
59
- await $`mv package.json.bak package.json`;
60
- await $`mv pnpm-lock.yaml.bak pnpm-lock.yaml`;
61
- if (closeVersion) {
62
- await $`pnpm changeset pre exit`;
63
- }
64
-
65
- try {
66
- const releases = await versionStatus();
67
-
68
- await $`pnpm changeset version`;
69
-
70
- await removeChangesetOutput();
71
-
72
- return releases;
73
- } catch {
74
- return [];
75
- }
76
- }
77
-
78
- export async function updateJiraWithVersion(releases) {
79
- const lastCommit = (await $`git log -1 --pretty=%B`).stdout.trim();
80
- console.log(`Last commit '${lastCommit}' being processed to get Jira ticket`);
81
-
82
- const jiraTicketId = findJiraTicketId(lastCommit);
83
-
84
- // If we have all the necessary data, update the component version in Jira
85
- if (jiraTicketId) {
86
- for (const release of releases) {
87
- try {
88
- await updateComponentVersion(
89
- jiraTicketId,
90
- release.name,
91
- release.newVersion,
92
- );
93
- } catch (error) {
94
- console.error(
95
- `Failed to update Jira component version: ${error.message}`,
96
- );
97
- }
98
- }
99
- }
100
-
101
- console.log('✅ Changeset versioning complete.');
102
- }
package/src/index.mjs DELETED
@@ -1,2 +0,0 @@
1
- import changelogCanny from './changeset/changelog-canny.js';
2
- export default changelogCanny;
@@ -1,17 +0,0 @@
1
- import { $ as original$ } from 'zx';
2
-
3
- // Wrapped version of $
4
- export const $ = async (strings, ...values) => {
5
- const command = strings.reduce(
6
- (acc, str, i) => acc + str + (values[i] ?? ''),
7
- '',
8
- );
9
- console.log(`Executing: ${command}`);
10
- try {
11
- const result = await original$(strings, ...values);
12
- return result;
13
- } catch (error) {
14
- console.error(`Error executing: ${command}`, error);
15
- throw error;
16
- }
17
- };