genaicode 0.0.31 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -10
- package/bin/vertex-monkey-patch.cjs +1 -1
- package/package.json +6 -3
- package/src/ai-service/anthropic.js +6 -3
- package/src/ai-service/chat-gpt.js +5 -2
- package/src/ai-service/common.test.js +94 -0
- package/src/ai-service/dall-e.js +29 -8
- package/src/ai-service/function-calling.js +137 -10
- package/src/ai-service/vertex-ai-claude.js +5 -2
- package/src/ai-service/vertex-ai-imagen.js +78 -0
- package/src/ai-service/vertex-ai.js +9 -8
- package/src/cli/cli-options.js +14 -0
- package/src/cli/cli-options.test.js +25 -0
- package/src/cli/cli-params.js +19 -4
- package/src/cli/validate-cli-params.js +28 -1
- package/src/cli/validate-cli-params.test.js +31 -0
- package/src/files/file-utils.js +7 -0
- package/src/files/file-utils.test.js +21 -0
- package/src/files/find-files.js +4 -66
- package/src/files/read-files.js +20 -1
- package/src/files/read-files.test.js +102 -0
- package/src/files/temp-buffer.js +13 -0
- package/src/files/update-files.js +103 -16
- package/src/images/ensure-alpha.js +5 -0
- package/src/images/imgly-remove-background.js +23 -0
- package/src/images/resize-image.js +23 -0
- package/src/images/split-image.js +26 -0
- package/src/main/codegen.js +10 -3
- package/src/main/codegen.test.js +154 -6
- package/src/main/config-lib.js +33 -0
- package/src/main/config-lib.test.js +80 -0
- package/src/main/config.js +39 -0
- package/src/prompt/prompt-codegen.js +1 -1
- package/src/prompt/prompt-service.js +18 -9
- package/src/prompt/prompt-service.test.js +25 -11
- package/src/prompt/systemprompt.js +2 -2
- package/src/prompt/systemprompt.test.js +5 -2
|
@@ -21,5 +21,30 @@ describe('CLI Options', () => {
|
|
|
21
21
|
|
|
22
22
|
consoleSpy.mockRestore();
|
|
23
23
|
});
|
|
24
|
+
|
|
25
|
+
it('should include the --imagen option in the help message', () => {
|
|
26
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
27
|
+
|
|
28
|
+
printHelpMessage();
|
|
29
|
+
|
|
30
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('--imagen=<service>'));
|
|
31
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
32
|
+
expect.stringContaining(
|
|
33
|
+
'Enable image generation functionality and specify the service to use (either "vertex-ai" or "dall-e").',
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
consoleSpy.mockRestore();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('cliOptions', () => {
|
|
42
|
+
it('should include the --imagen option', () => {
|
|
43
|
+
const imagenOption = cliOptions.find((option) => option.name === '--imagen=<service>');
|
|
44
|
+
expect(imagenOption).toBeDefined();
|
|
45
|
+
expect(imagenOption.description).toBe(
|
|
46
|
+
'Enable image generation functionality and specify the service to use (either "vertex-ai" or "dall-e").',
|
|
47
|
+
);
|
|
48
|
+
});
|
|
24
49
|
});
|
|
25
50
|
});
|
package/src/cli/cli-params.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
|
|
2
|
+
import path from 'path';
|
|
3
3
|
import { serviceAutoDetect } from './service-autodetect.js';
|
|
4
|
-
import { rcConfig } from '../
|
|
4
|
+
import { rcConfig } from '../main/config.js';
|
|
5
5
|
|
|
6
6
|
const params = process.argv.slice(2);
|
|
7
7
|
|
|
@@ -19,12 +19,13 @@ export const dependencyTree = params.includes('--dependency-tree');
|
|
|
19
19
|
export const verbosePrompt = params.includes('--verbose-prompt');
|
|
20
20
|
export let explicitPrompt = params.find((param) => param.startsWith('--explicit-prompt'))?.split('=')[1];
|
|
21
21
|
export const disableContextOptimization = params.includes('--disable-context-optimization');
|
|
22
|
-
export
|
|
22
|
+
export let taskFile = params.find((param) => param.startsWith('--task-file'))?.split('=')[1];
|
|
23
23
|
export const requireExplanations = params.includes('--require-explanations');
|
|
24
24
|
export const geminiBlockNone = params.includes('--gemini-block-none');
|
|
25
25
|
export const disableInitialLint = params.includes('--disable-initial-lint');
|
|
26
26
|
export const vision = params.includes('--vision');
|
|
27
|
-
export const imagen = params.
|
|
27
|
+
export const imagen = params.find((param) => param.startsWith('--imagen'))?.split('=')[1];
|
|
28
|
+
export const cheap = params.includes('--cheap');
|
|
28
29
|
|
|
29
30
|
// Add support for --help option
|
|
30
31
|
export const helpRequested = params.includes('--help');
|
|
@@ -37,6 +38,9 @@ export const temperature = parseFloat(
|
|
|
37
38
|
params.find((param) => param.startsWith('--temperature='))?.split('=')[1] || '0.7',
|
|
38
39
|
); // Default temperature value: 0.7
|
|
39
40
|
|
|
41
|
+
// New content mask parameter
|
|
42
|
+
export const contentMask = params.find((param) => param.startsWith('--content-mask='))?.split('=')[1] || null;
|
|
43
|
+
|
|
40
44
|
if (taskFile) {
|
|
41
45
|
if (explicitPrompt) {
|
|
42
46
|
throw new Error('The --task-file option is exclusive with the --explicit-prompt option');
|
|
@@ -44,6 +48,9 @@ if (taskFile) {
|
|
|
44
48
|
if (!fs.existsSync(taskFile)) {
|
|
45
49
|
throw new Error(`The task file ${taskFile} does not exist`);
|
|
46
50
|
}
|
|
51
|
+
if (!path.isAbsolute(taskFile)) {
|
|
52
|
+
taskFile = path.join(process.cwd(), taskFile);
|
|
53
|
+
}
|
|
47
54
|
explicitPrompt = `I want you to perform a coding task. The task is described in the ${taskFile} file. Use those instructions.`;
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -82,3 +89,11 @@ if (temperature) {
|
|
|
82
89
|
if (imagen) {
|
|
83
90
|
console.log('Image generation functionality enabled');
|
|
84
91
|
}
|
|
92
|
+
|
|
93
|
+
if (cheap) {
|
|
94
|
+
console.log('Cheaper AI models will be used for content and image generation');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (contentMask) {
|
|
98
|
+
console.log(`Content mask: ${contentMask}`);
|
|
99
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { rcConfig } from '../main/config.js';
|
|
4
|
+
|
|
1
5
|
// List of allowed CLI parameters
|
|
2
6
|
const allowedParameters = [
|
|
3
7
|
'--dry-run',
|
|
@@ -20,8 +24,10 @@ const allowedParameters = [
|
|
|
20
24
|
'--disable-initial-lint',
|
|
21
25
|
'--temperature=',
|
|
22
26
|
'--vision',
|
|
23
|
-
'--imagen',
|
|
27
|
+
'--imagen=',
|
|
28
|
+
'--cheap',
|
|
24
29
|
'--help',
|
|
30
|
+
'--content-mask=',
|
|
25
31
|
];
|
|
26
32
|
|
|
27
33
|
/**
|
|
@@ -65,9 +71,30 @@ export function validateCliParams() {
|
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
// Validate --imagen parameter
|
|
75
|
+
const imagenParam = providedParameters.find((param) => param.startsWith('--imagen='));
|
|
76
|
+
if (imagenParam) {
|
|
77
|
+
const imagenValue = imagenParam.split('=')[1];
|
|
78
|
+
if (imagenValue !== 'vertex-ai' && imagenValue !== 'dall-e') {
|
|
79
|
+
throw new Error('Invalid --imagen value. It must be either "vertex-ai" or "dall-e".');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
if (providedParameters.includes('--vision') && providedParameters.includes('--vertex-ai')) {
|
|
69
84
|
throw new Error('--vision and --vertex-ai are currently not supported together.');
|
|
70
85
|
}
|
|
86
|
+
|
|
87
|
+
// Validate content mask parameter
|
|
88
|
+
const contentMaskParam = providedParameters.find((param) => param.startsWith('--content-mask='));
|
|
89
|
+
if (contentMaskParam) {
|
|
90
|
+
const contentMaskValue = contentMaskParam.split('=')[1];
|
|
91
|
+
const fullPath = path.join(rcConfig.rootDir, contentMaskValue);
|
|
92
|
+
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isDirectory()) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Invalid --content-mask value. The path "${contentMaskValue}" does not exist or is not a directory within the project.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
71
98
|
}
|
|
72
99
|
|
|
73
100
|
/**
|
|
@@ -43,6 +43,27 @@ describe('validateCliParams', () => {
|
|
|
43
43
|
|
|
44
44
|
expect(() => validateCliParams()).toThrow('--vision and --vertex-ai are currently not supported together.');
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
// New tests for --imagen parameter
|
|
48
|
+
it('should not throw for valid --imagen parameter with vertex-ai', () => {
|
|
49
|
+
process.argv.push('--imagen=vertex-ai');
|
|
50
|
+
expect(() => validateCliParams()).not.toThrow();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should not throw for valid --imagen parameter with dall-e', () => {
|
|
54
|
+
process.argv.push('--imagen=dall-e');
|
|
55
|
+
expect(() => validateCliParams()).not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw for invalid --imagen parameter value', () => {
|
|
59
|
+
process.argv.push('--imagen=invalid-service');
|
|
60
|
+
expect(() => validateCliParams()).toThrow('Invalid --imagen value. It must be either "vertex-ai" or "dall-e".');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should throw for --imagen parameter without value', () => {
|
|
64
|
+
process.argv.push('--imagen=');
|
|
65
|
+
expect(() => validateCliParams()).toThrow('Invalid --imagen value. It must be either "vertex-ai" or "dall-e".');
|
|
66
|
+
});
|
|
46
67
|
});
|
|
47
68
|
|
|
48
69
|
describe('getCliParamValue', () => {
|
|
@@ -74,6 +95,11 @@ describe('getCliParamValue', () => {
|
|
|
74
95
|
process.argv.push('--temperature=0.5');
|
|
75
96
|
expect(getCliParamValue('--temperature')).toBe('0.5');
|
|
76
97
|
});
|
|
98
|
+
|
|
99
|
+
it('should return the value for --imagen parameter', () => {
|
|
100
|
+
process.argv.push('--imagen=vertex-ai');
|
|
101
|
+
expect(getCliParamValue('--imagen')).toBe('vertex-ai');
|
|
102
|
+
});
|
|
77
103
|
});
|
|
78
104
|
|
|
79
105
|
describe('hasCliParam', () => {
|
|
@@ -100,4 +126,9 @@ describe('hasCliParam', () => {
|
|
|
100
126
|
process.argv.push('--temperature=0.5');
|
|
101
127
|
expect(hasCliParam('--temperature')).toBe(true);
|
|
102
128
|
});
|
|
129
|
+
|
|
130
|
+
it('should return true for --imagen parameter', () => {
|
|
131
|
+
process.argv.push('--imagen=dall-e');
|
|
132
|
+
expect(hasCliParam('--imagen')).toBe(true);
|
|
133
|
+
});
|
|
103
134
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
/** Check if directory is ancestor of given directory */
|
|
4
|
+
export function isAncestorDirectory(parent, dir) {
|
|
5
|
+
const relative = path.relative(parent, dir);
|
|
6
|
+
return parent === dir || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
|
2
|
+
import { isAncestorDirectory } from './file-utils.js';
|
|
3
|
+
|
|
4
|
+
// Test for isAncestorDirectory
|
|
5
|
+
describe('isAncestorDirectory', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.resetAllMocks();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should return true if directories are the same', () => {
|
|
11
|
+
expect(isAncestorDirectory('/project', '/project')).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should return true if parent is ancestor of dir', () => {
|
|
15
|
+
expect(isAncestorDirectory('/project', '/project/src')).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should return false if parent is not ancestor of dir', () => {
|
|
19
|
+
expect(isAncestorDirectory('/project', '/other')).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
package/src/files/find-files.js
CHANGED
|
@@ -1,73 +1,17 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
// This file contains project codegen configuration
|
|
6
|
-
const CODEGENRC_FILENAME = '.genaicoderc';
|
|
7
|
-
|
|
8
|
-
const cwd = process.cwd();
|
|
3
|
+
import { rcConfig, sourceExtensions, IMAGE_ASSET_EXTENSIONS, ignorePaths } from '../main/config.js';
|
|
9
4
|
|
|
10
5
|
// List of possible extensions for dependency resolution
|
|
11
6
|
const POSSIBLE_DEPENDENCY_EXTENSIONS = ['.ts', '.js', '.tsx', '.jsx'];
|
|
12
7
|
|
|
13
|
-
// Find .genaicoderc file
|
|
14
|
-
let rcFilePath = cwd;
|
|
15
|
-
while (!fs.existsSync(path.join(rcFilePath, CODEGENRC_FILENAME))) {
|
|
16
|
-
const parentDir = path.dirname(rcFilePath);
|
|
17
|
-
if (parentDir === rcFilePath) {
|
|
18
|
-
throw new Error(`${CODEGENRC_FILENAME} not found in any parent directory`);
|
|
19
|
-
}
|
|
20
|
-
rcFilePath = parentDir;
|
|
21
|
-
}
|
|
22
|
-
rcFilePath = path.join(rcFilePath, CODEGENRC_FILENAME);
|
|
23
|
-
|
|
24
|
-
assert(fs.existsSync(rcFilePath), `${CODEGENRC_FILENAME} not found`);
|
|
25
|
-
|
|
26
|
-
// Read rootDir and extensions from .genaicoderc
|
|
27
|
-
export const rcConfig = JSON.parse(fs.readFileSync(rcFilePath, 'utf-8'));
|
|
28
|
-
export const rootDir = path.resolve(path.dirname(rcFilePath), rcConfig.rootDir);
|
|
29
|
-
|
|
30
|
-
assert(rootDir, 'Root dir not configured');
|
|
31
|
-
assert(isAncestorDirectory(path.dirname(rcFilePath), rootDir), 'Root dir is not located inside project directory');
|
|
32
|
-
|
|
33
|
-
console.log('Detected codegen configuration', rcConfig);
|
|
34
|
-
console.log('Root dir:', rootDir);
|
|
35
|
-
|
|
36
|
-
// Default extensions if not specified in .genaicoderc
|
|
37
|
-
const DEFAULT_EXTENSIONS = [
|
|
38
|
-
'.md',
|
|
39
|
-
'.js',
|
|
40
|
-
'.ts',
|
|
41
|
-
'.tsx',
|
|
42
|
-
'.css',
|
|
43
|
-
'.scss',
|
|
44
|
-
'.py',
|
|
45
|
-
'.go',
|
|
46
|
-
'.c',
|
|
47
|
-
'.h',
|
|
48
|
-
'.cpp',
|
|
49
|
-
'.txt',
|
|
50
|
-
'.html',
|
|
51
|
-
'.txt',
|
|
52
|
-
'.json',
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
// Use extensions from .genaicoderc if available, otherwise use default
|
|
56
|
-
const sourceExtensions = rcConfig.extensions || DEFAULT_EXTENSIONS;
|
|
57
|
-
|
|
58
|
-
// Image extensions (driven by ai service limitations)
|
|
59
|
-
const IMAGE_ASSET_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
|
|
60
|
-
|
|
61
|
-
// A list of paths that are ignored by default
|
|
62
|
-
const DEFAULT_IGNORE_PATHS = ['node_modules', 'build', 'dist', 'package-lock.json', 'coverage'];
|
|
63
|
-
|
|
64
8
|
function findFiles(dir, recursive, extensions) {
|
|
65
9
|
const files = [];
|
|
66
10
|
const items = fs.readdirSync(dir);
|
|
67
11
|
for (const item of items) {
|
|
68
12
|
const fullPath = path.join(dir, item);
|
|
69
13
|
|
|
70
|
-
if (
|
|
14
|
+
if (ignorePaths.some((ignorePath) => fullPath.endsWith(ignorePath))) {
|
|
71
15
|
continue;
|
|
72
16
|
}
|
|
73
17
|
|
|
@@ -127,22 +71,16 @@ export function getDependencyList(entryFile) {
|
|
|
127
71
|
return Array.from(result);
|
|
128
72
|
}
|
|
129
73
|
|
|
130
|
-
const sourceFiles = findFiles(rootDir, true, sourceExtensions);
|
|
74
|
+
const sourceFiles = findFiles(rcConfig.rootDir, true, sourceExtensions);
|
|
131
75
|
|
|
132
76
|
/** Get source files of the application */
|
|
133
77
|
export function getSourceFiles() {
|
|
134
78
|
return [...sourceFiles];
|
|
135
79
|
}
|
|
136
80
|
|
|
137
|
-
const imageAssetFiles = findFiles(rootDir, true, IMAGE_ASSET_EXTENSIONS);
|
|
81
|
+
const imageAssetFiles = findFiles(rcConfig.rootDir, true, IMAGE_ASSET_EXTENSIONS);
|
|
138
82
|
|
|
139
83
|
/** Get source files of the application */
|
|
140
84
|
export function getImageAssetFiles() {
|
|
141
85
|
return [...imageAssetFiles];
|
|
142
86
|
}
|
|
143
|
-
|
|
144
|
-
/** Check if directory is ancestor of given directory */
|
|
145
|
-
export function isAncestorDirectory(parent, dir) {
|
|
146
|
-
const relative = path.relative(parent, dir);
|
|
147
|
-
return parent === dir || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
148
|
-
}
|
package/src/files/read-files.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import mime from 'mime-types';
|
|
3
3
|
import sizeOf from 'image-size';
|
|
4
|
+
import path from 'path';
|
|
4
5
|
|
|
5
6
|
import { getSourceFiles, getImageAssetFiles } from './find-files.js';
|
|
7
|
+
import { rcConfig } from '../main/config.js';
|
|
6
8
|
import { verifySourceCodeLimit } from '../prompt/limits.js';
|
|
9
|
+
import { taskFile, contentMask } from '../cli/cli-params.js';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Read contents of source files and create a map with file path as key and file content as value
|
|
@@ -12,7 +15,16 @@ function readSourceFiles(filterPaths) {
|
|
|
12
15
|
const sourceCode = {};
|
|
13
16
|
for (const file of getSourceFiles()) {
|
|
14
17
|
if (!filterPaths || filterPaths.includes(file)) {
|
|
15
|
-
|
|
18
|
+
// Apply content mask filter if it's set
|
|
19
|
+
if (!filterPaths && contentMask) {
|
|
20
|
+
const relativePath = path.relative(rcConfig.rootDir, file);
|
|
21
|
+
if (!relativePath.startsWith(contentMask)) {
|
|
22
|
+
sourceCode[file] = { content: null }; // Include the file path but set content to null
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
27
|
+
sourceCode[file] = { content };
|
|
16
28
|
}
|
|
17
29
|
}
|
|
18
30
|
return sourceCode;
|
|
@@ -21,6 +33,13 @@ function readSourceFiles(filterPaths) {
|
|
|
21
33
|
/** Print source code of all source files */
|
|
22
34
|
export function getSourceCode(filterPaths) {
|
|
23
35
|
const sourceCode = readSourceFiles(filterPaths);
|
|
36
|
+
|
|
37
|
+
if (taskFile && !sourceCode[taskFile]) {
|
|
38
|
+
sourceCode[taskFile] = {
|
|
39
|
+
content: fs.readFileSync(taskFile, 'utf-8'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
24
43
|
verifySourceCodeLimit(JSON.stringify(sourceCode));
|
|
25
44
|
return sourceCode;
|
|
26
45
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import mime from 'mime-types';
|
|
4
|
+
import sizeOf from 'image-size';
|
|
5
|
+
import { getSourceCode, getImageAssets } from './read-files.js';
|
|
6
|
+
import { getSourceFiles, getImageAssetFiles } from './find-files.js';
|
|
7
|
+
import { verifySourceCodeLimit } from '../prompt/limits.js';
|
|
8
|
+
import * as cliParams from '../cli/cli-params.js';
|
|
9
|
+
import { rcConfig } from '../main/config.js';
|
|
10
|
+
|
|
11
|
+
vi.mock('fs');
|
|
12
|
+
vi.mock('mime-types');
|
|
13
|
+
vi.mock('image-size');
|
|
14
|
+
vi.mock('./find-files.js', () => ({
|
|
15
|
+
getImageAssetFiles: vi.fn(),
|
|
16
|
+
getSourceFiles: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('../prompt/limits.js');
|
|
19
|
+
vi.mock('../cli/cli-params.js', () => ({
|
|
20
|
+
taskFile: null,
|
|
21
|
+
contentMask: null,
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('../main/config.js', () => ({
|
|
24
|
+
rcConfig: { rootDir: '/home/project' },
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe('read-files', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.resetAllMocks();
|
|
30
|
+
vi.mocked(cliParams).contentMask = undefined;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getSourceCode', () => {
|
|
38
|
+
it('should return source code for all files', () => {
|
|
39
|
+
const mockFiles = ['/home/project/file1.js', '/home/project/file2.js'];
|
|
40
|
+
getSourceFiles.mockReturnValue(mockFiles);
|
|
41
|
+
fs.readFileSync.mockImplementation((file) => `Content of ${file}`);
|
|
42
|
+
rcConfig.rootDir = '/home/project';
|
|
43
|
+
|
|
44
|
+
const result = getSourceCode();
|
|
45
|
+
|
|
46
|
+
expect(result).toEqual({
|
|
47
|
+
'/home/project/file1.js': { content: 'Content of /home/project/file1.js' },
|
|
48
|
+
'/home/project/file2.js': { content: 'Content of /home/project/file2.js' },
|
|
49
|
+
});
|
|
50
|
+
expect(verifySourceCodeLimit).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should apply content mask when specified', () => {
|
|
54
|
+
const mockFiles = ['/home/project/file1.js', '/home/project/subfolder/file2.js'];
|
|
55
|
+
getSourceFiles.mockReturnValue(mockFiles);
|
|
56
|
+
fs.readFileSync.mockImplementation((file) => `Content of ${file}`);
|
|
57
|
+
rcConfig.rootDir = '/home/project';
|
|
58
|
+
vi.mocked(cliParams).contentMask = 'subfolder';
|
|
59
|
+
|
|
60
|
+
const result = getSourceCode();
|
|
61
|
+
|
|
62
|
+
expect(result).toEqual({
|
|
63
|
+
'/home/project/file1.js': { content: null },
|
|
64
|
+
'/home/project/subfolder/file2.js': {
|
|
65
|
+
content: 'Content of /home/project/subfolder/file2.js',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should include task file when specified', () => {
|
|
71
|
+
const mockFiles = ['/home/project/file1.js'];
|
|
72
|
+
getSourceFiles.mockReturnValue(mockFiles);
|
|
73
|
+
fs.readFileSync.mockImplementation((file) => `Content of ${file}`);
|
|
74
|
+
rcConfig.rootDir = '/home/project';
|
|
75
|
+
vi.mocked(cliParams).taskFile = '/home/project/task.md';
|
|
76
|
+
|
|
77
|
+
const result = getSourceCode();
|
|
78
|
+
|
|
79
|
+
expect(result).toEqual({
|
|
80
|
+
'/home/project/file1.js': { content: 'Content of /home/project/file1.js' },
|
|
81
|
+
'/home/project/task.md': { content: 'Content of /home/project/task.md' },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('getImageAssets', () => {
|
|
87
|
+
it('should return image assets information', () => {
|
|
88
|
+
const mockImageFiles = ['/home/project/image1.png', '/home/project/image2.jpg'];
|
|
89
|
+
getImageAssetFiles.mockReturnValue(mockImageFiles);
|
|
90
|
+
mime.lookup.mockImplementation((file) => (file.endsWith('.png') ? 'image/png' : 'image/jpeg'));
|
|
91
|
+
sizeOf.mockImplementation(() => ({ width: 100, height: 200 }));
|
|
92
|
+
|
|
93
|
+
const result = getImageAssets();
|
|
94
|
+
|
|
95
|
+
expect(result).toEqual({
|
|
96
|
+
'/home/project/image1.png': { mimeType: 'image/png', width: 100, height: 200 },
|
|
97
|
+
'/home/project/image2.jpg': { mimeType: 'image/jpeg', width: 100, height: 200 },
|
|
98
|
+
});
|
|
99
|
+
expect(verifySourceCodeLimit).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Temporary storage */
|
|
2
|
+
let tempId = 1;
|
|
3
|
+
const temp = {};
|
|
4
|
+
|
|
5
|
+
export function getTempBuffer(url) {
|
|
6
|
+
return temp[url];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function setTempBuffer(buffer) {
|
|
10
|
+
const imageUrl = 'temp://' + tempId++;
|
|
11
|
+
temp[imageUrl] = buffer;
|
|
12
|
+
return imageUrl;
|
|
13
|
+
}
|
|
@@ -3,7 +3,9 @@ import path from 'path';
|
|
|
3
3
|
import assert from 'node:assert';
|
|
4
4
|
import * as diff from 'diff';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { getSourceFiles } from './find-files.js';
|
|
7
|
+
import { isAncestorDirectory } from './file-utils.js';
|
|
8
|
+
import { rcConfig } from '../main/config.js';
|
|
7
9
|
import {
|
|
8
10
|
allowDirectoryCreate,
|
|
9
11
|
allowFileCreate,
|
|
@@ -13,29 +15,61 @@ import {
|
|
|
13
15
|
chatGpt,
|
|
14
16
|
vertexAiClaude,
|
|
15
17
|
} from '../cli/cli-params.js';
|
|
18
|
+
import { getTempBuffer } from './temp-buffer.js';
|
|
19
|
+
import { imglyRemoveBackground } from '../images/imgly-remove-background.js';
|
|
20
|
+
import { splitImage } from '../images/split-image.js';
|
|
21
|
+
import { resizeImageFile } from '../images/resize-image.js';
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* @param functionCalls Result of the code generation, a map of file paths to new content
|
|
19
25
|
*/
|
|
20
26
|
export async function updateFiles(functionCalls) {
|
|
21
27
|
for (const { name, args } of functionCalls) {
|
|
22
|
-
let {
|
|
28
|
+
let {
|
|
29
|
+
filePath,
|
|
30
|
+
newContent,
|
|
31
|
+
source,
|
|
32
|
+
destination,
|
|
33
|
+
patch,
|
|
34
|
+
inputFilePath,
|
|
35
|
+
outputFilePath,
|
|
36
|
+
backgroundColor,
|
|
37
|
+
parts,
|
|
38
|
+
size,
|
|
39
|
+
} = args;
|
|
23
40
|
|
|
24
41
|
// Check if filePath is absolute, if not use rootDir as baseline
|
|
25
|
-
if (name !== 'moveFile') {
|
|
26
|
-
filePath = path.isAbsolute(filePath) ? filePath : path.join(rootDir, filePath);
|
|
27
|
-
} else {
|
|
28
|
-
source = path.isAbsolute(source) ? source : path.join(rootDir, source);
|
|
29
|
-
destination = path.isAbsolute(destination) ? destination : path.join(rootDir, destination);
|
|
42
|
+
if (name !== 'moveFile' && name !== 'imglyRemoveBackground' && name !== 'splitImage') {
|
|
43
|
+
filePath = path.isAbsolute(filePath) ? filePath : path.join(rcConfig.rootDir, filePath);
|
|
44
|
+
} else if (name === 'moveFile') {
|
|
45
|
+
source = path.isAbsolute(source) ? source : path.join(rcConfig.rootDir, source);
|
|
46
|
+
destination = path.isAbsolute(destination) ? destination : path.join(rcConfig.rootDir, destination);
|
|
47
|
+
} else if (name === 'imglyRemoveBackground' || name === 'splitImage') {
|
|
48
|
+
inputFilePath = path.isAbsolute(inputFilePath) ? inputFilePath : path.join(rcConfig.rootDir, inputFilePath);
|
|
49
|
+
if (name === 'imglyRemoveBackground') {
|
|
50
|
+
outputFilePath = path.isAbsolute(outputFilePath) ? outputFilePath : path.join(rcConfig.rootDir, outputFilePath);
|
|
51
|
+
} else if (name === 'splitImage') {
|
|
52
|
+
parts.forEach(
|
|
53
|
+
(part) =>
|
|
54
|
+
(part.outputFilePath = path.isAbsolute(part.outputFilePath)
|
|
55
|
+
? part.outputFilePath
|
|
56
|
+
: path.join(rcConfig.rootDir, part.outputFilePath)),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
30
59
|
}
|
|
31
60
|
|
|
32
61
|
// ignore files which are not located inside project directory (sourceFiles)
|
|
33
62
|
if (
|
|
34
|
-
(name !== 'moveFile' && !isProjectPath(filePath)) ||
|
|
35
|
-
(name === 'moveFile' && (!isProjectPath(source) || !isProjectPath(destination)))
|
|
63
|
+
(name !== 'moveFile' && name !== 'imglyRemoveBackground' && name !== 'splitImage' && !isProjectPath(filePath)) ||
|
|
64
|
+
(name === 'moveFile' && (!isProjectPath(source) || !isProjectPath(destination))) ||
|
|
65
|
+
(name === 'imglyRemoveBackground' && (!isProjectPath(inputFilePath) || !isProjectPath(outputFilePath))) ||
|
|
66
|
+
(name === 'splitImage' &&
|
|
67
|
+
(!isProjectPath(inputFilePath) || parts.some((part) => !isProjectPath(part.outputFilePath))))
|
|
36
68
|
) {
|
|
37
|
-
console.log(`Skipping file: ${filePath || source}`);
|
|
38
|
-
throw new Error(
|
|
69
|
+
console.log(`Skipping file: ${filePath || source || inputFilePath}`);
|
|
70
|
+
throw new Error(
|
|
71
|
+
`File ${filePath || source || inputFilePath} is not located inside project directory, something is wrong?`,
|
|
72
|
+
);
|
|
39
73
|
}
|
|
40
74
|
|
|
41
75
|
if (name === 'deleteFile') {
|
|
@@ -90,15 +124,68 @@ export async function updateFiles(functionCalls) {
|
|
|
90
124
|
}
|
|
91
125
|
try {
|
|
92
126
|
assert(args.downloadUrl, 'image url is not empty');
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
127
|
+
if (args.downloadUrl.startsWith('temp://')) {
|
|
128
|
+
assert(getTempBuffer(args.downloadUrl), 'Temp buffer not present but expected');
|
|
129
|
+
fs.writeFileSync(filePath, getTempBuffer(args.downloadUrl));
|
|
130
|
+
} else {
|
|
131
|
+
const imageResponse = await fetch(args.downloadUrl);
|
|
132
|
+
const arrayBuffer = await imageResponse.arrayBuffer();
|
|
133
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
134
|
+
fs.writeFileSync(filePath, buffer);
|
|
135
|
+
}
|
|
97
136
|
console.log(`Image download and saved to: ${filePath}`);
|
|
98
137
|
} catch (error) {
|
|
99
138
|
console.error(`Failed to download image: ${error.message}`);
|
|
100
139
|
throw error;
|
|
101
140
|
}
|
|
141
|
+
} else if (name === 'imglyRemoveBackground') {
|
|
142
|
+
console.log(`Removing background from image: ${inputFilePath}`);
|
|
143
|
+
assert(fs.existsSync(inputFilePath), 'Input file does not exist');
|
|
144
|
+
assert(
|
|
145
|
+
allowFileCreate || fs.existsSync(outputFilePath),
|
|
146
|
+
'File create option was not enabled and output file does not exist',
|
|
147
|
+
);
|
|
148
|
+
if (allowDirectoryCreate) {
|
|
149
|
+
fs.mkdirSync(path.dirname(outputFilePath), { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await imglyRemoveBackground(inputFilePath, outputFilePath, backgroundColor);
|
|
153
|
+
console.log(`Background removed and image saved to: ${outputFilePath}`);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`Failed to remove background: ${error.message}`);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
} else if (name === 'splitImage') {
|
|
159
|
+
console.log(`Splitting image: ${inputFilePath}`, parts);
|
|
160
|
+
assert(fs.existsSync(inputFilePath), 'Input file does not exist');
|
|
161
|
+
assert(Array.isArray(parts) && parts.length > 0, 'Parts array must not be empty');
|
|
162
|
+
for (const part of parts) {
|
|
163
|
+
assert(
|
|
164
|
+
allowFileCreate || fs.existsSync(part.outputFilePath),
|
|
165
|
+
'File create option was not enabled and output file does not exist',
|
|
166
|
+
);
|
|
167
|
+
if (allowDirectoryCreate) {
|
|
168
|
+
fs.mkdirSync(path.dirname(part.outputFilePath), { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
await splitImage(inputFilePath, parts);
|
|
173
|
+
console.log(`Image split successfully`);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(`Failed to split image: ${error.message}`);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
} else if (name === 'resizeImage') {
|
|
179
|
+
console.log(`Resizing image: ${filePath}`, size);
|
|
180
|
+
assert(fs.existsSync(filePath), 'Input file does not exist');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await resizeImageFile(filePath, size);
|
|
184
|
+
console.log(`Image resized successfully`);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error(`Failed to resize image: ${error.message}`);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
102
189
|
}
|
|
103
190
|
}
|
|
104
191
|
}
|
|
@@ -107,7 +194,7 @@ function isProjectPath(filePath) {
|
|
|
107
194
|
const sourceFiles = getSourceFiles();
|
|
108
195
|
|
|
109
196
|
return (
|
|
110
|
-
isAncestorDirectory(rootDir, filePath) ||
|
|
197
|
+
isAncestorDirectory(rcConfig.rootDir, filePath) ||
|
|
111
198
|
sourceFiles.includes(filePath) ||
|
|
112
199
|
!sourceFiles.some(
|
|
113
200
|
(sourceFile) =>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
import { removeBackground } from '@imgly/background-removal-node';
|
|
5
|
+
|
|
6
|
+
/** Converts white color to transparency on a image and saves to destination path */
|
|
7
|
+
export async function imglyRemoveBackground(inputFilePath, outputFilePath) {
|
|
8
|
+
try {
|
|
9
|
+
console.log(`Removing background for image: ${inputFilePath}`);
|
|
10
|
+
|
|
11
|
+
const publicPath = 'file://' + createRequire(import.meta.url).resolve('@imgly/background-removal-node');
|
|
12
|
+
const blob = await removeBackground(inputFilePath, {
|
|
13
|
+
publicPath,
|
|
14
|
+
});
|
|
15
|
+
fs.writeFileSync(outputFilePath, Buffer.from(await blob.arrayBuffer()));
|
|
16
|
+
|
|
17
|
+
console.log(`Background removed successfully. Saved to: ${outputFilePath}`);
|
|
18
|
+
return outputFilePath;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Error removing background:', error);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|