autotest-ai 1.0.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/README.md +262 -0
- package/dist/chunk-OAREENOQ.mjs +201 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +398 -0
- package/dist/cli.mjs +188 -0
- package/dist/index.d.mts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +237 -0
- package/dist/index.mjs +12 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# autotest-ai
|
|
2
|
+
|
|
3
|
+
**AI-powered test generator. Generate unit tests from your code using Claude or OpenAI.**
|
|
4
|
+
|
|
5
|
+
Stop writing tests manually. Let AI analyze your code and generate comprehensive unit tests.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/autotest-ai)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **AI-Powered**: Uses Claude or OpenAI to understand your code and generate meaningful tests
|
|
16
|
+
- **Multiple Frameworks**: Supports Jest, Vitest, Node.js test runner, and Mocha
|
|
17
|
+
- **Edge Cases**: Automatically generates tests for edge cases (null, undefined, empty values)
|
|
18
|
+
- **TypeScript Support**: Generates TypeScript or JavaScript tests
|
|
19
|
+
- **CLI & API**: Use from command line or programmatically
|
|
20
|
+
- **Batch Processing**: Generate tests for entire directories
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g autotest-ai
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or use with npx:
|
|
31
|
+
```bash
|
|
32
|
+
npx autotest-ai src/utils.ts
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
### 1. Set your API key
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# For Claude (recommended)
|
|
43
|
+
export ANTHROPIC_API_KEY=your-api-key
|
|
44
|
+
|
|
45
|
+
# For OpenAI
|
|
46
|
+
export OPENAI_API_KEY=your-api-key
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Generate tests
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Generate tests for a file
|
|
53
|
+
autotest src/utils.ts
|
|
54
|
+
|
|
55
|
+
# Generate tests for all files in a directory
|
|
56
|
+
autotest src/
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Run your tests
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm test
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## CLI Usage
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
autotest <file|directory> [options]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Options
|
|
74
|
+
|
|
75
|
+
| Option | Description |
|
|
76
|
+
|--------|-------------|
|
|
77
|
+
| `-p, --provider <claude\|openai>` | AI provider (default: claude) |
|
|
78
|
+
| `-m, --model <model>` | Model to use |
|
|
79
|
+
| `-f, --framework <framework>` | Test framework: jest, vitest, node, mocha |
|
|
80
|
+
| `-o, --output <dir>` | Output directory (default: __tests__) |
|
|
81
|
+
| `--no-edge-cases` | Skip edge case tests |
|
|
82
|
+
| `--js` | Generate JavaScript instead of TypeScript |
|
|
83
|
+
| `--dry-run` | Print tests without writing files |
|
|
84
|
+
| `-h, --help` | Show help |
|
|
85
|
+
|
|
86
|
+
### Examples
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Generate Jest tests (default)
|
|
90
|
+
autotest src/utils.ts
|
|
91
|
+
|
|
92
|
+
# Generate Vitest tests
|
|
93
|
+
autotest src/utils.ts --framework vitest
|
|
94
|
+
|
|
95
|
+
# Use OpenAI instead of Claude
|
|
96
|
+
autotest src/utils.ts --provider openai
|
|
97
|
+
|
|
98
|
+
# Preview generated tests without writing
|
|
99
|
+
autotest src/utils.ts --dry-run
|
|
100
|
+
|
|
101
|
+
# Generate tests for entire src directory
|
|
102
|
+
autotest src/
|
|
103
|
+
|
|
104
|
+
# Generate JavaScript tests
|
|
105
|
+
autotest src/utils.js --js
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Programmatic API
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { generateTests, generateAndWriteTests } from 'autotest-ai';
|
|
114
|
+
|
|
115
|
+
// Generate tests (returns test code)
|
|
116
|
+
const result = await generateTests('src/utils.ts', {
|
|
117
|
+
provider: 'claude',
|
|
118
|
+
framework: 'jest',
|
|
119
|
+
edgeCases: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
console.log(result.testCode);
|
|
123
|
+
console.log(`Functions tested: ${result.functions.join(', ')}`);
|
|
124
|
+
|
|
125
|
+
// Generate and write tests to file
|
|
126
|
+
const written = await generateAndWriteTests('src/utils.ts', {
|
|
127
|
+
framework: 'vitest',
|
|
128
|
+
outputDir: '__tests__',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.log(`Test file created: ${written.testFile}`);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### API Reference
|
|
135
|
+
|
|
136
|
+
#### `generateTests(filePath, config?)`
|
|
137
|
+
|
|
138
|
+
Generates tests for a file and returns the result.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
interface GenerateResult {
|
|
142
|
+
sourceFile: string; // Original source file path
|
|
143
|
+
testFile: string; // Generated test file path
|
|
144
|
+
testCode: string; // Generated test code
|
|
145
|
+
functions: string[]; // Functions that were tested
|
|
146
|
+
tokensUsed?: number; // API tokens used
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### `generateAndWriteTests(filePath, config?)`
|
|
151
|
+
|
|
152
|
+
Generates tests and writes them to a file.
|
|
153
|
+
|
|
154
|
+
#### `generateTestsForDirectory(dirPath, config?)`
|
|
155
|
+
|
|
156
|
+
Generates tests for all source files in a directory.
|
|
157
|
+
|
|
158
|
+
### Config Options
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface AutoTestConfig {
|
|
162
|
+
provider?: 'claude' | 'openai'; // AI provider
|
|
163
|
+
apiKey?: string; // API key (or use env var)
|
|
164
|
+
model?: string; // Model to use
|
|
165
|
+
framework?: 'jest' | 'vitest' | 'node' | 'mocha';
|
|
166
|
+
outputDir?: string; // Output directory
|
|
167
|
+
edgeCases?: boolean; // Include edge cases
|
|
168
|
+
typescript?: boolean; // Generate TypeScript
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Example Output
|
|
175
|
+
|
|
176
|
+
### Input: `src/math.ts`
|
|
177
|
+
```typescript
|
|
178
|
+
export function add(a: number, b: number): number {
|
|
179
|
+
return a + b;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function divide(a: number, b: number): number {
|
|
183
|
+
if (b === 0) throw new Error('Division by zero');
|
|
184
|
+
return a / b;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Generated: `__tests__/math.test.ts`
|
|
189
|
+
```typescript
|
|
190
|
+
import { describe, it, expect } from '@jest/globals';
|
|
191
|
+
import { add, divide } from '../src/math';
|
|
192
|
+
|
|
193
|
+
describe('add', () => {
|
|
194
|
+
it('should add two positive numbers', () => {
|
|
195
|
+
expect(add(2, 3)).toBe(5);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should add negative numbers', () => {
|
|
199
|
+
expect(add(-1, -1)).toBe(-2);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle zero', () => {
|
|
203
|
+
expect(add(0, 5)).toBe(5);
|
|
204
|
+
expect(add(5, 0)).toBe(5);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('divide', () => {
|
|
209
|
+
it('should divide two numbers', () => {
|
|
210
|
+
expect(divide(10, 2)).toBe(5);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should handle decimal results', () => {
|
|
214
|
+
expect(divide(5, 2)).toBe(2.5);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should throw error for division by zero', () => {
|
|
218
|
+
expect(() => divide(10, 0)).toThrow('Division by zero');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Supported Test Frameworks
|
|
226
|
+
|
|
227
|
+
| Framework | Command |
|
|
228
|
+
|-----------|---------|
|
|
229
|
+
| **Jest** | `autotest file.ts --framework jest` |
|
|
230
|
+
| **Vitest** | `autotest file.ts --framework vitest` |
|
|
231
|
+
| **Node.js** | `autotest file.ts --framework node` |
|
|
232
|
+
| **Mocha** | `autotest file.ts --framework mocha` |
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Environment Variables
|
|
237
|
+
|
|
238
|
+
| Variable | Description |
|
|
239
|
+
|----------|-------------|
|
|
240
|
+
| `ANTHROPIC_API_KEY` | API key for Claude |
|
|
241
|
+
| `OPENAI_API_KEY` | API key for OpenAI |
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Tips
|
|
246
|
+
|
|
247
|
+
1. **Better tests with better code**: Well-documented functions with clear types produce better tests
|
|
248
|
+
2. **Review generated tests**: AI-generated tests are a starting point, review and adjust as needed
|
|
249
|
+
3. **Use --dry-run first**: Preview tests before writing to files
|
|
250
|
+
4. **Combine with TDD**: Generate initial tests, then refine as you develop
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Support
|
|
255
|
+
|
|
256
|
+
If autotest-ai saved you time:
|
|
257
|
+
- Star the [GitHub repo](https://github.com/willzhangfly/autotest-ai)
|
|
258
|
+
- [Buy me a coffee](https://buymeacoffee.com/willzhangfly)
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
MIT
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
var DEFAULT_CONFIG = {
|
|
5
|
+
provider: "claude",
|
|
6
|
+
apiKey: "",
|
|
7
|
+
model: "claude-sonnet-4-20250514",
|
|
8
|
+
framework: "jest",
|
|
9
|
+
outputDir: "__tests__",
|
|
10
|
+
edgeCases: true,
|
|
11
|
+
typescript: true
|
|
12
|
+
};
|
|
13
|
+
async function callClaude(prompt, apiKey, model) {
|
|
14
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
"x-api-key": apiKey,
|
|
19
|
+
"anthropic-version": "2023-06-01"
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
model,
|
|
23
|
+
max_tokens: 4096,
|
|
24
|
+
messages: [{ role: "user", content: prompt }]
|
|
25
|
+
})
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
const error = await response.text();
|
|
29
|
+
throw new Error(`Claude API error: ${response.status} - ${error}`);
|
|
30
|
+
}
|
|
31
|
+
const data = await response.json();
|
|
32
|
+
return {
|
|
33
|
+
content: data.content[0].text,
|
|
34
|
+
tokensUsed: data.usage.input_tokens + data.usage.output_tokens
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async function callOpenAI(prompt, apiKey, model) {
|
|
38
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"Authorization": `Bearer ${apiKey}`
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
model,
|
|
46
|
+
max_tokens: 4096,
|
|
47
|
+
messages: [{ role: "user", content: prompt }]
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const error = await response.text();
|
|
52
|
+
throw new Error(`OpenAI API error: ${response.status} - ${error}`);
|
|
53
|
+
}
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
return {
|
|
56
|
+
content: data.choices[0].message.content,
|
|
57
|
+
tokensUsed: data.usage.total_tokens
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function buildPrompt(code, framework, edgeCases, typescript) {
|
|
61
|
+
const frameworkImports = {
|
|
62
|
+
jest: `import { describe, it, expect } from '@jest/globals';`,
|
|
63
|
+
vitest: `import { describe, it, expect } from 'vitest';`,
|
|
64
|
+
node: `import { describe, it } from 'node:test';
|
|
65
|
+
import assert from 'node:assert';`,
|
|
66
|
+
mocha: `import { describe, it } from 'mocha';
|
|
67
|
+
import { expect } from 'chai';`
|
|
68
|
+
};
|
|
69
|
+
const assertStyle = framework === "node" ? "assert" : "expect";
|
|
70
|
+
return `You are an expert test writer. Generate comprehensive unit tests for the following code.
|
|
71
|
+
|
|
72
|
+
## Requirements:
|
|
73
|
+
- Use ${framework} test framework
|
|
74
|
+
- ${typescript ? "Use TypeScript" : "Use JavaScript"}
|
|
75
|
+
- Import the functions being tested from the source file
|
|
76
|
+
- ${edgeCases ? "Include edge cases (null, undefined, empty, boundary values)" : "Focus on happy path tests"}
|
|
77
|
+
- Each test should have a clear, descriptive name
|
|
78
|
+
- Use ${assertStyle} style assertions
|
|
79
|
+
- Group related tests in describe blocks
|
|
80
|
+
|
|
81
|
+
## Framework imports:
|
|
82
|
+
${frameworkImports[framework]}
|
|
83
|
+
|
|
84
|
+
## Source Code:
|
|
85
|
+
\`\`\`${typescript ? "typescript" : "javascript"}
|
|
86
|
+
${code}
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
## Output:
|
|
90
|
+
Generate ONLY the test code, no explanations. Start with imports and end with the last test.
|
|
91
|
+
The test file should be complete and runnable.
|
|
92
|
+
Import functions from '../src/filename' (adjust path as needed).`;
|
|
93
|
+
}
|
|
94
|
+
function extractFunctionNames(code) {
|
|
95
|
+
const functions = [];
|
|
96
|
+
const funcDecl = /function\s+(\w+)\s*\(/g;
|
|
97
|
+
let match;
|
|
98
|
+
while ((match = funcDecl.exec(code)) !== null) {
|
|
99
|
+
functions.push(match[1]);
|
|
100
|
+
}
|
|
101
|
+
const arrowFunc = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(/g;
|
|
102
|
+
while ((match = arrowFunc.exec(code)) !== null) {
|
|
103
|
+
functions.push(match[1]);
|
|
104
|
+
}
|
|
105
|
+
const arrowSingle = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\w+\s*=>/g;
|
|
106
|
+
while ((match = arrowSingle.exec(code)) !== null) {
|
|
107
|
+
functions.push(match[1]);
|
|
108
|
+
}
|
|
109
|
+
const exportFunc = /export\s+(?:async\s+)?function\s+(\w+)/g;
|
|
110
|
+
while ((match = exportFunc.exec(code)) !== null) {
|
|
111
|
+
if (!functions.includes(match[1])) {
|
|
112
|
+
functions.push(match[1]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const methods = /^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*{/gm;
|
|
116
|
+
while ((match = methods.exec(code)) !== null) {
|
|
117
|
+
if (!["constructor", "if", "for", "while", "switch"].includes(match[1])) {
|
|
118
|
+
if (!functions.includes(match[1])) {
|
|
119
|
+
functions.push(match[1]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...new Set(functions)];
|
|
124
|
+
}
|
|
125
|
+
async function generateTests(filePath, config = {}) {
|
|
126
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
127
|
+
const apiKey = cfg.apiKey || (cfg.provider === "claude" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY) || "";
|
|
128
|
+
if (!apiKey) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`API key required. Set ${cfg.provider === "claude" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} environment variable or pass apiKey in config.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const absolutePath = path.resolve(filePath);
|
|
134
|
+
if (!fs.existsSync(absolutePath)) {
|
|
135
|
+
throw new Error(`File not found: ${absolutePath}`);
|
|
136
|
+
}
|
|
137
|
+
const code = fs.readFileSync(absolutePath, "utf-8");
|
|
138
|
+
const functions = extractFunctionNames(code);
|
|
139
|
+
if (functions.length === 0) {
|
|
140
|
+
throw new Error(`No functions found in ${filePath}`);
|
|
141
|
+
}
|
|
142
|
+
const prompt = buildPrompt(code, cfg.framework, cfg.edgeCases, cfg.typescript);
|
|
143
|
+
const model = cfg.model || (cfg.provider === "claude" ? "claude-sonnet-4-20250514" : "gpt-4o");
|
|
144
|
+
const callAI = cfg.provider === "claude" ? callClaude : callOpenAI;
|
|
145
|
+
const result = await callAI(prompt, apiKey, model);
|
|
146
|
+
let testCode = result.content;
|
|
147
|
+
const codeBlockMatch = testCode.match(/```(?:typescript|javascript|ts|js)?\n([\s\S]*?)```/);
|
|
148
|
+
if (codeBlockMatch) {
|
|
149
|
+
testCode = codeBlockMatch[1];
|
|
150
|
+
}
|
|
151
|
+
const ext = cfg.typescript ? ".test.ts" : ".test.js";
|
|
152
|
+
const baseName = path.basename(filePath, path.extname(filePath));
|
|
153
|
+
const testFileName = baseName + ext;
|
|
154
|
+
const testFilePath = path.join(path.dirname(absolutePath), cfg.outputDir, testFileName);
|
|
155
|
+
return {
|
|
156
|
+
sourceFile: absolutePath,
|
|
157
|
+
testFile: testFilePath,
|
|
158
|
+
testCode: testCode.trim(),
|
|
159
|
+
functions,
|
|
160
|
+
tokensUsed: result.tokensUsed
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async function generateAndWriteTests(filePath, config = {}) {
|
|
164
|
+
const result = await generateTests(filePath, config);
|
|
165
|
+
const outputDir = path.dirname(result.testFile);
|
|
166
|
+
if (!fs.existsSync(outputDir)) {
|
|
167
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
fs.writeFileSync(result.testFile, result.testCode);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
async function generateTestsForDirectory(dirPath, config = {}) {
|
|
173
|
+
const results = [];
|
|
174
|
+
const absolutePath = path.resolve(dirPath);
|
|
175
|
+
const files = fs.readdirSync(absolutePath);
|
|
176
|
+
const sourceFiles = files.filter(
|
|
177
|
+
(f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.endsWith(".test.ts") && !f.endsWith(".test.js") && !f.endsWith(".spec.ts") && !f.endsWith(".spec.js") && !f.endsWith(".d.ts")
|
|
178
|
+
);
|
|
179
|
+
for (const file of sourceFiles) {
|
|
180
|
+
const filePath = path.join(absolutePath, file);
|
|
181
|
+
try {
|
|
182
|
+
const result = await generateAndWriteTests(filePath, config);
|
|
183
|
+
results.push(result);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(`Error generating tests for ${file}:`, error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
var index_default = {
|
|
191
|
+
generateTests,
|
|
192
|
+
generateAndWriteTests,
|
|
193
|
+
generateTestsForDirectory
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export {
|
|
197
|
+
generateTests,
|
|
198
|
+
generateAndWriteTests,
|
|
199
|
+
generateTestsForDirectory,
|
|
200
|
+
index_default
|
|
201
|
+
};
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|