@nexical/ai 0.1.0
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/.github/workflows/deploy.yml +34 -0
- package/.husky/pre-commit +2 -0
- package/dist/AiClientFactory.js +10 -0
- package/dist/index.js +3 -0
- package/dist/providers/GeminiCLI.js +52 -0
- package/dist/types.js +1 -0
- package/eslint.config.mjs +67 -0
- package/package.json +42 -0
- package/src/AiClientFactory.ts +14 -0
- package/src/index.ts +3 -0
- package/src/providers/GeminiCLI.ts +66 -0
- package/src/types.ts +15 -0
- package/tests/providers/GeminiCLI.test.ts +120 -0
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Deploy Nexical AI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
name: Build & Publish
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout Repo
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Setup Node.js
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 22
|
|
20
|
+
registry-url: 'https://registry.npmjs.org'
|
|
21
|
+
|
|
22
|
+
- name: Install Dependencies
|
|
23
|
+
run: npm install
|
|
24
|
+
|
|
25
|
+
- name: Build
|
|
26
|
+
run: npm run build
|
|
27
|
+
|
|
28
|
+
- name: Test
|
|
29
|
+
run: npm run test
|
|
30
|
+
|
|
31
|
+
- name: Publish to NPM
|
|
32
|
+
run: npm publish --access public
|
|
33
|
+
env:
|
|
34
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GeminiCLI } from './providers/GeminiCLI.js';
|
|
2
|
+
export class AiClientFactory {
|
|
3
|
+
static create(config) {
|
|
4
|
+
const provider = config?.provider || 'gemini-cli';
|
|
5
|
+
if (provider === 'gemini-cli') {
|
|
6
|
+
return new GeminiCLI(config || {});
|
|
7
|
+
}
|
|
8
|
+
throw new Error(`Unsupported AI Client provider: ${provider}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
export class GeminiCLI {
|
|
4
|
+
commandTemplate;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.commandTemplate = config.commandTemplate || 'gemini --yolo -p "" --model {model}';
|
|
7
|
+
}
|
|
8
|
+
run(model, input) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const command = this.commandTemplate.replace('{model}', model);
|
|
11
|
+
const start = Date.now();
|
|
12
|
+
const child = spawn(command, {
|
|
13
|
+
shell: true,
|
|
14
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
15
|
+
});
|
|
16
|
+
let stdoutData = '';
|
|
17
|
+
let stderrData = '';
|
|
18
|
+
child.stdout?.on('data', (data) => {
|
|
19
|
+
const chunk = data.toString();
|
|
20
|
+
process.stdout.write(chalk.yellow(chunk));
|
|
21
|
+
stdoutData += chunk;
|
|
22
|
+
});
|
|
23
|
+
child.stderr?.on('data', (data) => {
|
|
24
|
+
const chunk = data.toString();
|
|
25
|
+
stderrData += chunk;
|
|
26
|
+
});
|
|
27
|
+
child.stdin.write(input);
|
|
28
|
+
child.stdin.end();
|
|
29
|
+
child.on('close', (code) => {
|
|
30
|
+
const duration = Date.now() - start;
|
|
31
|
+
const exitCode = code ?? 1;
|
|
32
|
+
const isExhausted = stderrData.includes('429') ||
|
|
33
|
+
stderrData.includes('exhausted your capacity') ||
|
|
34
|
+
stderrData.includes('ResourceExhausted');
|
|
35
|
+
if (exitCode !== 0 && isExhausted) {
|
|
36
|
+
console.warn(`[Agent] Model ${model} exhausted (429). Duration: ${duration}ms`);
|
|
37
|
+
resolve({ code: exitCode, shouldRetry: true, output: stdoutData });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
if (exitCode !== 0 && stderrData) {
|
|
41
|
+
process.stderr.write(stderrData);
|
|
42
|
+
}
|
|
43
|
+
resolve({ code: exitCode, shouldRetry: false, output: stdoutData });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
child.on('error', (err) => {
|
|
47
|
+
console.error(`[Agent] Failed to spawn Gemini (${model}): ${err instanceof Error ? err.message : String(err)}`);
|
|
48
|
+
resolve({ code: 1, shouldRetry: false, output: '' });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import eslintPluginAstro from 'eslint-plugin-astro';
|
|
2
|
+
import eslintPluginReact from 'eslint-plugin-react';
|
|
3
|
+
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
|
|
4
|
+
import eslintPluginJsxA11y from 'eslint-plugin-jsx-a11y';
|
|
5
|
+
import eslintConfigPrettier from 'eslint-config-prettier';
|
|
6
|
+
import tseslint from 'typescript-eslint';
|
|
7
|
+
import globals from 'globals';
|
|
8
|
+
import js from '@eslint/js';
|
|
9
|
+
|
|
10
|
+
export default tseslint.config(
|
|
11
|
+
// Global ignore
|
|
12
|
+
{
|
|
13
|
+
ignores: ['**/dist/**', '**/node_modules/**', '**/coverage/**', '**/.astro/**', '**/.agent/**'],
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
// Base JS/TS configuration
|
|
17
|
+
js.configs.recommended,
|
|
18
|
+
...tseslint.configs.recommended,
|
|
19
|
+
|
|
20
|
+
// React configuration
|
|
21
|
+
{
|
|
22
|
+
files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
|
|
23
|
+
plugins: {
|
|
24
|
+
react: eslintPluginReact,
|
|
25
|
+
'react-hooks': eslintPluginReactHooks,
|
|
26
|
+
'jsx-a11y': eslintPluginJsxA11y,
|
|
27
|
+
},
|
|
28
|
+
languageOptions: {
|
|
29
|
+
parserOptions: {
|
|
30
|
+
ecmaFeatures: {
|
|
31
|
+
jsx: true,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
globals: {
|
|
35
|
+
...globals.browser,
|
|
36
|
+
...globals.node,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
settings: {
|
|
40
|
+
react: {
|
|
41
|
+
version: 'detect',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
rules: {
|
|
45
|
+
...eslintPluginReact.configs.recommended.rules,
|
|
46
|
+
...eslintPluginReactHooks.configs.recommended.rules,
|
|
47
|
+
...eslintPluginJsxA11y.configs.recommended.rules,
|
|
48
|
+
'react/react-in-jsx-scope': 'off', // Not needed in React 17+ or Astro
|
|
49
|
+
'react/no-unknown-property': 'off', // Conflicts with some Astro/Web Component patterns
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Astro configuration
|
|
54
|
+
...eslintPluginAstro.configs.recommended,
|
|
55
|
+
|
|
56
|
+
// Custom rules override
|
|
57
|
+
{
|
|
58
|
+
rules: {
|
|
59
|
+
'@typescript-eslint/no-unused-vars': ['error', { args: 'none', varsIgnorePattern: '^_' }],
|
|
60
|
+
'@typescript-eslint/no-explicit-any': 'warn', // Downgrade to warning for now as codebase has some anys
|
|
61
|
+
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Prettier must be last to override conflicting rules
|
|
66
|
+
eslintConfigPrettier,
|
|
67
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexical/ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "General purpose AI Client interfaces and providers for the Nexical applications",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.build.json",
|
|
9
|
+
"format": "prettier --write .",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"lint:fix": "eslint . --fix",
|
|
12
|
+
"prepare": "husky",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"test": "vitest run --passWithNoTests",
|
|
15
|
+
"test:unit": "vitest run --passWithNoTests"
|
|
16
|
+
},
|
|
17
|
+
"lint-staged": {
|
|
18
|
+
"**/*": [
|
|
19
|
+
"prettier --write --ignore-unknown"
|
|
20
|
+
],
|
|
21
|
+
"**/*.{js,jsx,ts,tsx,astro}": [
|
|
22
|
+
"eslint --fix"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"chalk": "^5.3.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@eslint/js": "^9.39.2",
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"eslint": "^9.39.2",
|
|
32
|
+
"eslint-config-prettier": "^10.0.1",
|
|
33
|
+
"globals": "^15.9.0",
|
|
34
|
+
"husky": "^9.1.5",
|
|
35
|
+
"lint-staged": "^15.2.10",
|
|
36
|
+
"prettier": "^3.3.3",
|
|
37
|
+
"typescript": "^5.5.4",
|
|
38
|
+
"typescript-eslint": "^8.3.0",
|
|
39
|
+
"vitest": "^2.1.0"
|
|
40
|
+
},
|
|
41
|
+
"types": "dist/index.d.ts"
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AiClient, AiClientConfig } from './types.js';
|
|
2
|
+
import { GeminiCLI } from './providers/GeminiCLI.js';
|
|
3
|
+
|
|
4
|
+
export class AiClientFactory {
|
|
5
|
+
static create(config?: AiClientConfig): AiClient {
|
|
6
|
+
const provider = config?.provider || 'gemini-cli';
|
|
7
|
+
|
|
8
|
+
if (provider === 'gemini-cli') {
|
|
9
|
+
return new GeminiCLI(config || {});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
throw new Error(`Unsupported AI Client provider: ${provider}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { AiClient, AiClientConfig, AiClientResult } from '../types.js';
|
|
4
|
+
|
|
5
|
+
export class GeminiCLI implements AiClient {
|
|
6
|
+
private commandTemplate: string;
|
|
7
|
+
|
|
8
|
+
constructor(config: AiClientConfig) {
|
|
9
|
+
this.commandTemplate = config.commandTemplate || 'gemini --yolo -p "" --model {model}';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
run(model: string, input: string): Promise<AiClientResult> {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const command = this.commandTemplate.replace('{model}', model);
|
|
15
|
+
const start = Date.now();
|
|
16
|
+
|
|
17
|
+
const child = spawn(command, {
|
|
18
|
+
shell: true,
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
let stdoutData = '';
|
|
23
|
+
let stderrData = '';
|
|
24
|
+
|
|
25
|
+
child.stdout?.on('data', (data) => {
|
|
26
|
+
const chunk = data.toString();
|
|
27
|
+
process.stdout.write(chalk.yellow(chunk));
|
|
28
|
+
stdoutData += chunk;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.stderr?.on('data', (data) => {
|
|
32
|
+
const chunk = data.toString();
|
|
33
|
+
stderrData += chunk;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
child.stdin.write(input);
|
|
37
|
+
child.stdin.end();
|
|
38
|
+
|
|
39
|
+
child.on('close', (code) => {
|
|
40
|
+
const duration = Date.now() - start;
|
|
41
|
+
const exitCode = code ?? 1;
|
|
42
|
+
const isExhausted =
|
|
43
|
+
stderrData.includes('429') ||
|
|
44
|
+
stderrData.includes('exhausted your capacity') ||
|
|
45
|
+
stderrData.includes('ResourceExhausted');
|
|
46
|
+
|
|
47
|
+
if (exitCode !== 0 && isExhausted) {
|
|
48
|
+
console.warn(`[Agent] Model ${model} exhausted (429). Duration: ${duration}ms`);
|
|
49
|
+
resolve({ code: exitCode, shouldRetry: true, output: stdoutData });
|
|
50
|
+
} else {
|
|
51
|
+
if (exitCode !== 0 && stderrData) {
|
|
52
|
+
process.stderr.write(stderrData);
|
|
53
|
+
}
|
|
54
|
+
resolve({ code: exitCode, shouldRetry: false, output: stdoutData });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
child.on('error', (err) => {
|
|
59
|
+
console.error(
|
|
60
|
+
`[Agent] Failed to spawn Gemini (${model}): ${err instanceof Error ? err.message : String(err)}`,
|
|
61
|
+
);
|
|
62
|
+
resolve({ code: 1, shouldRetry: false, output: '' });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AiClientResult {
|
|
2
|
+
code: number;
|
|
3
|
+
shouldRetry: boolean;
|
|
4
|
+
output: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface AiClientConfig {
|
|
8
|
+
provider?: string;
|
|
9
|
+
commandTemplate?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AiClient {
|
|
14
|
+
run(model: string, input: string): Promise<AiClientResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { GeminiCLI } from '../../src/providers/GeminiCLI.js';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
vi.mock('node:child_process', () => ({
|
|
6
|
+
spawn: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('GeminiCLI', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.resetAllMocks();
|
|
12
|
+
// Prevent stdout/stderr intercepts from breaking the test output
|
|
13
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
14
|
+
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should run a prompt and succeed', async () => {
|
|
18
|
+
const mockChild = {
|
|
19
|
+
stdout: { on: vi.fn((event, cb) => cb('mock response')) },
|
|
20
|
+
stderr: { on: vi.fn() },
|
|
21
|
+
stdin: { write: vi.fn(), end: vi.fn() },
|
|
22
|
+
on: vi.fn((event, callback) => {
|
|
23
|
+
if (event === 'close') callback(0);
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
|
|
27
|
+
|
|
28
|
+
const client = new GeminiCLI({});
|
|
29
|
+
const result = await client.run('gemini-3-pro-preview', 'Hello World');
|
|
30
|
+
|
|
31
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
32
|
+
expect.stringContaining('gemini --yolo -p "" --model'),
|
|
33
|
+
expect.any(Object),
|
|
34
|
+
);
|
|
35
|
+
expect(result.code).toBe(0);
|
|
36
|
+
expect(result.shouldRetry).toBe(false);
|
|
37
|
+
expect(result.output).toBe('mock response');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return retry status on 429 ResourceExhausted', async () => {
|
|
41
|
+
const mockChild = {
|
|
42
|
+
stdout: { on: vi.fn() },
|
|
43
|
+
stderr: {
|
|
44
|
+
on: vi.fn((event, cb) => {
|
|
45
|
+
if (event === 'data') cb('Error: ResourceExhausted (429)');
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
stdin: { write: vi.fn(), end: vi.fn() },
|
|
49
|
+
on: vi.fn((event, callback) => {
|
|
50
|
+
if (event === 'close') callback(1);
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
|
|
54
|
+
|
|
55
|
+
const client = new GeminiCLI({});
|
|
56
|
+
const result = await client.run('gemini-3-pro-preview', 'Hello World');
|
|
57
|
+
|
|
58
|
+
expect(result.code).toBe(1);
|
|
59
|
+
expect(result.shouldRetry).toBe(true);
|
|
60
|
+
expect(result.output).toBe('');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return failure without retry on other errors', async () => {
|
|
64
|
+
const mockChild = {
|
|
65
|
+
stdout: { on: vi.fn() },
|
|
66
|
+
stderr: {
|
|
67
|
+
on: vi.fn((event, cb) => {
|
|
68
|
+
if (event === 'data') cb('Other fatal model error');
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
stdin: { write: vi.fn(), end: vi.fn() },
|
|
72
|
+
on: vi.fn((event, callback) => {
|
|
73
|
+
if (event === 'close') callback(2);
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
|
|
77
|
+
|
|
78
|
+
const client = new GeminiCLI({});
|
|
79
|
+
const result = await client.run('gemini-3-pro-preview', 'Hello World');
|
|
80
|
+
|
|
81
|
+
expect(result.code).toBe(2);
|
|
82
|
+
expect(result.shouldRetry).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should catch spawn errors', async () => {
|
|
86
|
+
const mockChild = {
|
|
87
|
+
stdout: { on: vi.fn() },
|
|
88
|
+
stderr: { on: vi.fn() },
|
|
89
|
+
stdin: { write: vi.fn(), end: vi.fn() },
|
|
90
|
+
on: vi.fn((event, callback) => {
|
|
91
|
+
if (event === 'error') callback(new Error('Spawn failed'));
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
|
|
95
|
+
|
|
96
|
+
const client = new GeminiCLI({});
|
|
97
|
+
const result = await client.run('gemini-3-pro-preview', 'Hello World');
|
|
98
|
+
|
|
99
|
+
expect(result.code).toBe(1);
|
|
100
|
+
expect(result.shouldRetry).toBe(false);
|
|
101
|
+
expect(result.output).toBe('');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should use a custom command template', async () => {
|
|
105
|
+
const mockChild = {
|
|
106
|
+
stdout: { on: vi.fn() },
|
|
107
|
+
stderr: { on: vi.fn() },
|
|
108
|
+
stdin: { write: vi.fn(), end: vi.fn() },
|
|
109
|
+
on: vi.fn((event, callback) => {
|
|
110
|
+
if (event === 'close') callback(0);
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
|
|
114
|
+
|
|
115
|
+
const client = new GeminiCLI({ commandTemplate: 'echo {model}' });
|
|
116
|
+
await client.run('gemini-3-pro-preview', 'Hello World');
|
|
117
|
+
|
|
118
|
+
expect(spawn).toHaveBeenCalledWith('echo gemini-3-pro-preview', expect.any(Object));
|
|
119
|
+
});
|
|
120
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ESNext"
|
|
8
|
+
],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"outDir": "dist",
|
|
15
|
+
"rootDir": "src"
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"src/**/*"
|
|
19
|
+
]
|
|
20
|
+
}
|