nayan-ai 1.0.0-beta.1 → 1.0.0-beta.3

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/src/codex.ts DELETED
@@ -1,158 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import type { CodeIssue } from './types.js';
3
- import { logProcessor, type CodexEvent } from './logs.js';
4
- import { getReviewPrompt } from './prompt.js';
5
-
6
- export interface CodexOptions {
7
- verbose?: boolean;
8
- }
9
-
10
- export async function analyzeWithCodex(
11
- repoPath: string,
12
- baseBranch: string = 'origin/main',
13
- options: CodexOptions
14
- ): Promise<CodeIssue[]> {
15
- const response = await runCodexExec(repoPath, baseBranch, options);
16
- return parseCodexResponse(response);
17
- }
18
-
19
- async function runCodexExec(
20
- repoPath: string,
21
- baseBranch: string,
22
- options: CodexOptions
23
- ): Promise<string> {
24
- const prompt = getReviewPrompt(baseBranch);
25
-
26
- return new Promise((resolve, reject) => {
27
- // Use codex exec with --json for structured output
28
- const args = ['@openai/codex', 'exec', '--json', '--full-auto', prompt];
29
-
30
- if (options.verbose) {
31
- console.log(`\n[Codex] Running: npx ${args.join(' ')}`);
32
- console.log(`[Codex] Working directory: ${repoPath}`);
33
- }
34
-
35
- const startTime = Date.now();
36
-
37
- const child = spawn('npx', args, {
38
- cwd: repoPath,
39
- stdio: ['pipe', 'pipe', 'pipe'],
40
- });
41
-
42
- let stdout = '';
43
- let stderr = '';
44
-
45
- // Reset log processor for new analysis
46
- logProcessor.reset();
47
-
48
- child.stdout.on('data', (data) => {
49
- const chunk = data.toString();
50
- stdout += chunk;
51
-
52
- // Parse JSONL and show progress
53
- for (const line of chunk.split('\n')) {
54
- if (!line.trim()) continue;
55
- try {
56
- const event: CodexEvent = JSON.parse(line);
57
-
58
- if (options.verbose) {
59
- console.log(JSON.stringify(event, null, 2));
60
- } else {
61
- // Use log processor for nice interactive output
62
- logProcessor.processEvent(event);
63
- }
64
- } catch {
65
- // Not JSON
66
- }
67
- }
68
- });
69
-
70
- child.stderr.on('data', (data) => {
71
- const chunk = data.toString();
72
- stderr += chunk;
73
- if (options.verbose) {
74
- process.stderr.write(chunk);
75
- }
76
- });
77
-
78
- child.on('close', (code) => {
79
- const elapsed = Date.now() - startTime;
80
-
81
- console.log(`\n[Codex] Completed in ${(elapsed / 1000).toFixed(1)}s`);
82
-
83
- if (code !== 0) {
84
- const errorMsg = stderr || stdout || 'Unknown error';
85
- reject(new Error(`Codex review failed (exit ${code}): ${errorMsg.slice(0, 500)}`));
86
- return;
87
- }
88
-
89
- resolve(stdout);
90
- });
91
-
92
- child.on('error', (err) => {
93
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
94
- reject(new Error('npx not found. Install Node.js/npm (Node 18+) to run nayan-ai.'));
95
- return;
96
- }
97
- reject(err);
98
- });
99
- });
100
- }
101
-
102
- function parseCodexResponse(response: string): CodeIssue[] {
103
- // codex exec --json outputs JSONL - parse each line looking for agent_message with issues
104
- const lines = response.split('\n');
105
-
106
- for (const line of lines) {
107
- if (!line.trim()) continue;
108
-
109
- try {
110
- const event = JSON.parse(line);
111
-
112
- // Look for agent_message in item.completed events
113
- if (event.type === 'item.completed' && event.item?.type === 'agent_message') {
114
- const text = event.item.text;
115
- if (text) {
116
- // Parse the JSON from the agent's message
117
- const issuesJson = JSON.parse(text);
118
- if (issuesJson.issues && Array.isArray(issuesJson.issues)) {
119
- return issuesJson.issues
120
- .filter((item: any) => item.message)
121
- .map((item: any) => ({
122
- filename: item.filename || 'unknown',
123
- line: item.line || 0,
124
- category: item.category || 'functionality',
125
- severity: item.severity || 'info',
126
- message: item.message,
127
- suggestion: item.suggestion,
128
- }));
129
- }
130
- }
131
- }
132
- } catch {
133
- // Not valid JSON, skip
134
- }
135
- }
136
-
137
- // Fallback: try to find raw JSON in the response
138
- const jsonMatch = response.match(/\{\s*"issues"\s*:\s*\[[\s\S]*?\]\s*\}/);
139
- if (jsonMatch) {
140
- try {
141
- const parsed = JSON.parse(jsonMatch[0]);
142
- return (parsed.issues || [])
143
- .filter((item: any) => item.message)
144
- .map((item: any) => ({
145
- filename: item.filename || 'unknown',
146
- line: item.line || 0,
147
- category: item.category || 'functionality',
148
- severity: item.severity || 'info',
149
- message: item.message,
150
- suggestion: item.suggestion,
151
- }));
152
- } catch {
153
- // ignore
154
- }
155
- }
156
-
157
- return [];
158
- }
package/src/github.ts DELETED
@@ -1,108 +0,0 @@
1
- import type { PRInfo, PullRequest, PullRequestFile, FileChange, ReviewComment } from './types.js';
2
-
3
- export class GitHubClient {
4
- private token: string;
5
- private apiBase: string;
6
-
7
- constructor(token: string, githubUrl?: string) {
8
- this.token = token;
9
- this.apiBase = githubUrl
10
- ? `${githubUrl.replace(/\/$/, '')}/api/v3`
11
- : 'https://api.github.com';
12
- }
13
-
14
- private async fetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
15
- const url = `${this.apiBase}${endpoint}`;
16
- const response = await fetch(url, {
17
- ...options,
18
- headers: {
19
- 'Accept': 'application/vnd.github.v3+json',
20
- 'Authorization': `Bearer ${this.token}`,
21
- 'User-Agent': 'nayan-ai',
22
- ...options.headers,
23
- },
24
- });
25
-
26
- if (!response.ok) {
27
- const body = await response.text();
28
- throw new Error(`GitHub API error (${response.status}): ${body}`);
29
- }
30
-
31
- return response.json() as Promise<T>;
32
- }
33
-
34
- async getPullRequest(pr: PRInfo): Promise<PullRequest> {
35
- return this.fetch<PullRequest>(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}`);
36
- }
37
-
38
- async getPullRequestFiles(pr: PRInfo): Promise<PullRequestFile[]> {
39
- return this.fetch<PullRequestFile[]>(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}/files?per_page=100`);
40
- }
41
-
42
- async postReview(
43
- pr: PRInfo,
44
- commitId: string,
45
- body: string,
46
- comments: ReviewComment[]
47
- ): Promise<void> {
48
- await this.fetch(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}/reviews`, {
49
- method: 'POST',
50
- headers: { 'Content-Type': 'application/json' },
51
- body: JSON.stringify({
52
- commit_id: commitId,
53
- body,
54
- event: 'COMMENT',
55
- comments,
56
- }),
57
- });
58
- }
59
-
60
- async postComment(pr: PRInfo, body: string): Promise<void> {
61
- await this.fetch(`/repos/${pr.owner}/${pr.repo}/issues/${pr.number}/comments`, {
62
- method: 'POST',
63
- headers: { 'Content-Type': 'application/json' },
64
- body: JSON.stringify({ body }),
65
- });
66
- }
67
- }
68
-
69
- export function parseFiles(files: PullRequestFile[]): FileChange[] {
70
- return files
71
- .filter((f): f is PullRequestFile & { patch: string } => !!f.patch)
72
- .map((f) => ({
73
- filename: f.filename,
74
- patch: f.patch,
75
- }));
76
- }
77
-
78
- export function parsePRReference(input: string): PRInfo & { githubUrl?: string } {
79
- // Full URL: https://github.com/owner/repo/pull/123 or https://enterprise.example.com/owner/repo/pull/123
80
- const urlMatch = input.match(/^(https?:\/\/[^/]+)\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
81
- if (urlMatch) {
82
- const baseUrl = urlMatch[1];
83
- // Only github.com (exactly) is public GitHub, everything else is enterprise
84
- const isEnterprise = !baseUrl.match(/^https?:\/\/(www\.)?github\.com$/i);
85
- return {
86
- owner: urlMatch[2],
87
- repo: urlMatch[3],
88
- number: parseInt(urlMatch[4], 10),
89
- githubUrl: isEnterprise ? baseUrl : undefined,
90
- };
91
- }
92
-
93
- // Short reference: owner/repo#123
94
- const shortMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
95
- if (shortMatch) {
96
- return {
97
- owner: shortMatch[1],
98
- repo: shortMatch[2],
99
- number: parseInt(shortMatch[3], 10),
100
- };
101
- }
102
-
103
- throw new Error(
104
- 'Invalid PR reference. Use:\n' +
105
- ' - https://github.com/owner/repo/pull/123\n' +
106
- ' - owner/repo#123'
107
- );
108
- }
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export { GitHubClient, parseFiles, parsePRReference } from './github.js';
2
- export { analyzeWithCodex } from './codex.js';
3
- export { analyzeWithClaude } from './claude.js';
4
- export { issuesToReviewComments, generateSummary } from './analyzer.js';
5
- export { cloneRepo, getGitDiff, getChangedFiles } from './repo.js';
6
- export type * from './types.js';
package/src/logs.ts DELETED
@@ -1,253 +0,0 @@
1
- import chalk from 'chalk';
2
- import ora, { type Ora } from 'ora';
3
-
4
- export interface CodexEvent {
5
- type: string;
6
- item?: {
7
- id?: string;
8
- type?: string;
9
- text?: string;
10
- command?: string;
11
- aggregated_output?: string;
12
- exit_code?: number | null;
13
- status?: string;
14
- };
15
- thread_id?: string;
16
- }
17
-
18
- export class LogProcessor {
19
- private seenFiles = new Set<string>();
20
- private currentPhase = '';
21
- private currentSpinner: Ora | null = null;
22
- private fileCount = 0;
23
- private analyzedCount = 0;
24
-
25
- processEvent(event: CodexEvent): void {
26
- switch (event.type) {
27
- case 'thread.started':
28
- this.startSpinner('Starting analysis...');
29
- break;
30
-
31
- case 'turn.started':
32
- // Silent - just marks the start of a turn
33
- break;
34
-
35
- case 'item.completed':
36
- this.handleItemCompleted(event);
37
- break;
38
-
39
- case 'item.started':
40
- this.handleItemStarted(event);
41
- break;
42
-
43
- case 'turn.completed':
44
- // Analysis complete - handled elsewhere
45
- break;
46
- }
47
- }
48
-
49
- private startSpinner(text: string): void {
50
- this.stopSpinner();
51
- this.currentSpinner = ora({
52
- text: chalk.cyan(text),
53
- prefixText: ' ',
54
- spinner: 'dots'
55
- }).start();
56
- }
57
-
58
- private succeedSpinner(text?: string): void {
59
- if (this.currentSpinner) {
60
- this.currentSpinner.succeed(text ? chalk.green(text) : undefined);
61
- this.currentSpinner = null;
62
- } else if (text) {
63
- console.log(chalk.green(` ✔ ${text}`));
64
- }
65
- }
66
-
67
- private stopSpinner(): void {
68
- if (this.currentSpinner) {
69
- this.currentSpinner.stop();
70
- this.currentSpinner = null;
71
- }
72
- }
73
-
74
- private logAndSpin(text: string): void {
75
- // First, succeed the current spinner to print it
76
- if (this.currentSpinner) {
77
- this.currentSpinner.succeed();
78
- }
79
- // Then start a new spinner with the new text
80
- this.currentSpinner = ora({
81
- text: chalk.cyan(text),
82
- prefixText: ' ',
83
- spinner: 'dots'
84
- }).start();
85
- }
86
-
87
- private handleItemStarted(event: CodexEvent): void {
88
- const item = event.item;
89
- if (!item) return;
90
-
91
- if (item.type === 'command_execution' && item.command) {
92
- const cmd = item.command;
93
-
94
- // Git diff command
95
- if (cmd.includes('git diff')) {
96
- this.setPhase('diff');
97
- this.logAndSpin('Checking differences...');
98
- }
99
- }
100
- }
101
-
102
- private handleItemCompleted(event: CodexEvent): void {
103
- const item = event.item;
104
- if (!item) return;
105
-
106
- if (item.type === 'reasoning' && item.text) {
107
- this.handleReasoning(item.text);
108
- } else if (item.type === 'command_execution') {
109
- this.handleCommandCompleted(item);
110
- } else if (item.type === 'agent_message') {
111
- this.succeedSpinner('Analysis complete');
112
- }
113
- }
114
-
115
- private handleReasoning(text: string): void {
116
- // Clean up the reasoning text - remove ** markers
117
- const cleanText = text.replace(/\*\*/g, '').trim();
118
-
119
- if (!cleanText) return;
120
-
121
- // Split into title (first line/sentence) and details
122
- const lines = cleanText.split('\n').filter(Boolean);
123
-
124
- // Check if it's a short title or has additional content
125
- if (lines.length === 1 && cleanText.length < 80) {
126
- // Short reasoning - just show as spinner
127
- this.logAndSpin(`💭 ${cleanText}`);
128
- } else {
129
- // Long reasoning - extract title and show details as bullet points
130
- let title = lines[0];
131
- let details = '';
132
-
133
- // If single line but long, split at first semicolon or period after 40 chars
134
- if (lines.length === 1) {
135
- const splitMatch = cleanText.match(/^(.{30,80}?[;.])\s*(.+)$/);
136
- if (splitMatch) {
137
- title = splitMatch[1];
138
- details = splitMatch[2];
139
- }
140
- } else {
141
- details = lines.slice(1).join(' ');
142
- }
143
-
144
- // Show title with spinner
145
- this.logAndSpin(`💭 ${title}`);
146
-
147
- // If there are details, print them as indented bullet points
148
- if (details) {
149
- // Succeed current spinner first
150
- if (this.currentSpinner) {
151
- this.currentSpinner.succeed();
152
- this.currentSpinner = null;
153
- }
154
- // Print details indented
155
- const detailLines = this.wrapText(details, 70);
156
- detailLines.forEach(line => {
157
- console.log(chalk.dim(` ${line}`));
158
- });
159
- }
160
- }
161
- }
162
-
163
- private wrapText(text: string, maxWidth: number): string[] {
164
- const words = text.split(' ');
165
- const lines: string[] = [];
166
- let currentLine = '';
167
-
168
- for (const word of words) {
169
- if (currentLine.length + word.length + 1 <= maxWidth) {
170
- currentLine += (currentLine ? ' ' : '') + word;
171
- } else {
172
- if (currentLine) lines.push(currentLine);
173
- currentLine = word;
174
- }
175
- }
176
- if (currentLine) lines.push(currentLine);
177
- return lines;
178
- }
179
-
180
- private handleCommandCompleted(item: CodexEvent['item']): void {
181
- if (!item) return;
182
-
183
- const cmd = item.command || '';
184
- const output = item.aggregated_output || '';
185
-
186
- // Git diff completed - show changed files count
187
- if (cmd.includes('git diff') && output) {
188
- const diffFiles = output.match(/diff --git a\/([^\s]+)/g);
189
- if (diffFiles && diffFiles.length > 0) {
190
- this.fileCount = diffFiles.length;
191
- this.succeedSpinner(`Found ${diffFiles.length} changed files`);
192
- }
193
- }
194
-
195
- // File listing with rg --files
196
- if (cmd.includes('rg --files') && output) {
197
- const files = output.trim().split('\n').filter(Boolean);
198
- if (files.length > 0) {
199
- this.succeedSpinner(`Identified ${files.length} files to analyze`);
200
- }
201
- }
202
-
203
- // File reading commands - extract and show file path
204
- const fileMatch = this.extractFilePath(cmd);
205
- if (fileMatch && !this.seenFiles.has(fileMatch)) {
206
- this.seenFiles.add(fileMatch);
207
- this.analyzedCount++;
208
- this.logAndSpin(`📄 Analyzing ${fileMatch}`);
209
- }
210
- }
211
-
212
- private extractFilePath(cmd: string): string | null {
213
- // Skip git commands - they're not file reads
214
- if (cmd.includes('git ')) return null;
215
-
216
- // Match patterns like:
217
- // sed -n '1,260p' path/to/file.tsx
218
- // nl -ba path/to/file.ts
219
- // cat path/to/file.ts
220
- const patterns = [
221
- /sed\s+-n\s+'[^']+'\s+([^\s|]+\.[a-z]{1,4})/i,
222
- /nl\s+-ba\s+([^\s|]+\.[a-z]{1,4})/i,
223
- /cat\s+([^\s|]+\.[a-z]{1,4})/i,
224
- ];
225
-
226
- for (const pattern of patterns) {
227
- const match = cmd.match(pattern);
228
- if (match && match[1]) {
229
- // Skip if it's a glob pattern or doesn't look like a real file
230
- if (match[1].includes('*')) continue;
231
- // Must have a proper file extension (1-4 chars)
232
- if (!/\.[a-z]{1,4}$/i.test(match[1])) continue;
233
- return match[1];
234
- }
235
- }
236
-
237
- return null;
238
- }
239
-
240
- private setPhase(phase: string): void {
241
- this.currentPhase = phase;
242
- }
243
-
244
- reset(): void {
245
- this.stopSpinner();
246
- this.seenFiles.clear();
247
- this.currentPhase = '';
248
- this.fileCount = 0;
249
- this.analyzedCount = 0;
250
- }
251
- }
252
-
253
- export const logProcessor = new LogProcessor();
package/src/prompt.ts DELETED
@@ -1,75 +0,0 @@
1
- export function getReviewPrompt(baseBranch: string): string {
2
- return `You are an expert code reviewer. Perform a comprehensive review of ALL code changes in this PR.
3
-
4
- STEP 1: Run "git diff -U10 ${baseBranch}...HEAD" to get all changed files with 10 lines of context.
5
-
6
- STEP 2: Analyze the diff directly (skip lock files like yarn.lock, package-lock.json, bun.lock):
7
- - The diff provides sufficient context for most issues
8
- - Only read the full file if you need more context to understand the change
9
- - Focus on the actual changes (+ and - lines) and their immediate context
10
-
11
- STEP 3: Check each change for these categories:
12
-
13
- **BUGS & LOGIC ERRORS:**
14
- - Off-by-one errors in loops/arrays
15
- - Null/undefined access without checks
16
- - Race conditions in async code
17
- - Incorrect boolean logic or conditions
18
- - Wrong variable usage or typos
19
- - Infinite loops or recursion without exit
20
- - Array mutation while iterating
21
-
22
- **ERROR HANDLING:**
23
- - Missing try/catch for async operations
24
- - Unhandled promise rejections
25
- - Missing error states in UI
26
- - Silent failures that should be logged
27
-
28
- **SECURITY:**
29
- - SQL injection, XSS vulnerabilities
30
- - Hardcoded secrets or credentials
31
- - Unsafe user input handling
32
- - Missing authentication/authorization checks
33
-
34
- **PERFORMANCE:**
35
- - Unnecessary re-renders or computations
36
- - Missing memoization where needed
37
- - N+1 query patterns
38
- - Memory leaks (event listeners, subscriptions not cleaned up)
39
- - Large objects in state that should be normalized
40
-
41
- **TYPE SAFETY:**
42
- - Any type usage that should be specific
43
- - Missing null checks for optional values
44
- - Type assertions that could fail at runtime
45
-
46
- **EDGE CASES:**
47
- - Empty arrays/objects not handled
48
- - Boundary conditions (0, negative, max values)
49
- - Network failures not handled
50
- - Loading/error states missing
51
-
52
- **CODE QUALITY:**
53
- - Dead code or unused variables
54
- - Duplicated logic that should be extracted
55
- - Overly complex functions that should be split
56
- - Missing cleanup in useEffect/subscriptions
57
-
58
- **TEST COVERAGE:**
59
- - Check if tests are added for new functionality
60
- - Check if existing tests need to be updated for changed code
61
- - Identify critical logic that should have unit tests
62
- - Flag functions with complex branching that need test coverage
63
- - Check if edge cases in the code have corresponding test cases
64
-
65
- STEP 4: Output ALL issues as JSON (be thorough - report every issue you find):
66
- {"issues":[{"filename":"<path>","line":<num>,"category":"functionality"|"performance"|"readability","severity":"error"|"warning"|"info","message":"<description>","suggestion":"<fix>"}]}
67
-
68
- **SEVERITY GUIDELINES (be consistent):**
69
- - **error**: Will cause bugs, crashes, security vulnerabilities, or data loss. Examples: null pointer access, unhandled exceptions, SQL injection, race conditions, infinite loops.
70
- - **warning**: Potential problems that may cause issues in edge cases or under certain conditions. Examples: missing error handling, possible memory leaks, missing null checks for optional values, performance issues.
71
- - **info**: Code quality improvements, best practices, style suggestions. Examples: dead code, code duplication, missing tests, readability improvements.
72
-
73
- CRITICAL: Review EVERY changed file in the diff. Do not skip any. Be efficient - analyze the diff context first, only read full files when truly needed.
74
- If truly no issues after thorough analysis, return {"issues":[]}`;
75
- }
package/src/repo.ts DELETED
@@ -1,81 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import { mkdtemp, rm } from 'fs/promises';
3
- import { tmpdir } from 'os';
4
- import { join } from 'path';
5
- import type { PRInfo } from './types.js';
6
-
7
- export interface ClonedRepo {
8
- path: string;
9
- cleanup: () => Promise<void>;
10
- }
11
-
12
- export async function cloneRepo(
13
- prInfo: PRInfo,
14
- token: string,
15
- githubUrl?: string
16
- ): Promise<ClonedRepo> {
17
- const tempDir = await mkdtemp(join(tmpdir(), 'nayan-ai-'));
18
-
19
- const baseUrl = githubUrl?.replace(/\/$/, '') || 'https://github.com';
20
- const cloneUrl = `https://${token}@${baseUrl.replace(/^https?:\/\//, '')}/${prInfo.owner}/${prInfo.repo}.git`;
21
-
22
- // Clone with enough depth to have main branch history
23
- await runGit(['clone', '--depth', '100', cloneUrl, tempDir]);
24
- // Fetch the main branch explicitly for comparison
25
- await runGit(['fetch', 'origin', 'main:refs/remotes/origin/main', '--depth', '100'], tempDir);
26
- // Fetch the PR branch
27
- await runGit(['fetch', 'origin', `pull/${prInfo.number}/head:pr-branch`], tempDir);
28
- await runGit(['checkout', 'pr-branch'], tempDir);
29
-
30
- return {
31
- path: tempDir,
32
- cleanup: async () => {
33
- try {
34
- await rm(tempDir, { recursive: true, force: true });
35
- } catch {
36
- // ignore cleanup errors
37
- }
38
- },
39
- };
40
- }
41
-
42
- export async function getGitDiff(repoPath: string): Promise<string> {
43
- return runGit(['diff', 'origin/main...HEAD', '--unified=3'], repoPath);
44
- }
45
-
46
- export async function getChangedFiles(repoPath: string): Promise<string[]> {
47
- const output = await runGit(['diff', 'origin/main...HEAD', '--name-only'], repoPath);
48
- return output.split('\n').filter(Boolean);
49
- }
50
-
51
- function runGit(args: string[], cwd?: string): Promise<string> {
52
- return new Promise((resolve, reject) => {
53
- const child = spawn('git', args, {
54
- cwd,
55
- stdio: ['pipe', 'pipe', 'pipe'],
56
- });
57
-
58
- let stdout = '';
59
- let stderr = '';
60
-
61
- child.stdout.on('data', (data) => {
62
- stdout += data.toString();
63
- });
64
-
65
- child.stderr.on('data', (data) => {
66
- stderr += data.toString();
67
- });
68
-
69
- child.on('close', (code) => {
70
- if (code !== 0) {
71
- reject(new Error(`git ${args[0]} failed: ${stderr || stdout || 'Unknown error'}`));
72
- return;
73
- }
74
- resolve(stdout);
75
- });
76
-
77
- child.on('error', (err) => {
78
- reject(err);
79
- });
80
- });
81
- }