devsplain 1.0.0 → 1.2.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 ADDED
@@ -0,0 +1,81 @@
1
+ # devsplain
2
+
3
+ ![devsplain demo](sample.gif)
4
+
5
+ An agent-agnostic CLI tool that automatically adds JSDoc and inline comments to your code using free LLMs.
6
+
7
+ Tired of writing documentation? Let AI do the explaining for you. `devsplain` reads your code and intelligently injects standard JSDoc headers and inline comments.
8
+
9
+ ## Installation
10
+
11
+ Install it globally via npm:
12
+
13
+ ```bash
14
+ npm install -g devsplain
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Simply point `devsplain` at any file you want to comment:
20
+
21
+ ```bash
22
+ devsplain src/utils.js
23
+ ```
24
+
25
+ ### Directory Support (Bulk Processing)
26
+
27
+ You don't have to go file-by-file! Point `devsplain` at an entire directory, and it will recursively crawl through your codebase and comment everything.
28
+
29
+ ```bash
30
+ devsplain src/
31
+ ```
32
+
33
+ _Note: `devsplain` is smart. It automatically ignores junk folders like `node_modules`, `.git`, `dist`, `venv`, and `.next` to save you time and API tokens._
34
+
35
+ ### Modes
36
+
37
+ You can control exactly how aggressive the AI is with its comments using flags:
38
+
39
+ - `--light`: Only adds JSDoc blocks above functions. Keeps the inside of your functions completely untouched.
40
+ - `--full`: Highly aggressive. Explains complex logic line-by-line inside your functions.
41
+ - `--clean`: A code scrubber. Removes ALL existing comments from the code, leaving it completely bare.
42
+ - `--dry-run`: Interactive preview. Prints the AI's output to the terminal and waits for your approval before saving to the file. Extremely safe for testing!
43
+ - **(Default)**: A balanced mix of JSDoc headers and sparse inline comments for complex logic.
44
+
45
+ **Usage Examples:**
46
+
47
+ ```bash
48
+ devsplain src/utils.js --light
49
+ devsplain src/ --full
50
+ devsplain legacy_code.js --clean
51
+ devsplain lib/ --dry-run
52
+ ```
53
+
54
+ ### Supported Languages
55
+
56
+ Because `devsplain` uses LLMs, it natively understands almost every language syntax. It currently processes the following extensions:
57
+
58
+ - **Web**: `.js`, `.jsx`, `.ts`, `.tsx`, `.html`, `.css`, `.scss`, `.vue`, `.svelte`
59
+ - **Backend**: `.py`, `.java`, `.c`, `.cpp`, `.cs`, `.go`, `.rb`, `.php`, `.rs`
60
+ - **Mobile/Scripts**: `.swift`, `.kt`, `.dart`, `.sh`
61
+
62
+ ## Agent Agnostic (Bring Your Own LLM)
63
+
64
+ `devsplain` doesn't lock you into one ecosystem. On your first run, an interactive Setup Wizard will ask you which engine you want to use:
65
+
66
+ 1. **Groq** (Recommended) - Instant, free Llama-3 endpoints.
67
+ 2. **Gemini** - Google's free tier endpoints.
68
+ 3. **OpenAI** - Standard paid ChatGPT models.
69
+ 4. **Custom** - Point it at ANY OpenAI-compatible endpoint (e.g., local models via Ollama or LMStudio).
70
+
71
+ _(Your configuration is safely stored in `~/.devsplainrc` on your machine)._
72
+
73
+ ---
74
+
75
+ ### Disclaimer
76
+
77
+ **Use at your own risk.** `devsplain` uses AI to physically overwrite your source files. While the prompts are heavily engineered to prevent code refactoring or corruption, AI is non-deterministic. **Always ensure your code is committed to Git (Version Control) before running `devsplain` on an entire directory!** We are not responsible for corrupted or lost code.
78
+
79
+ ## License
80
+
81
+ MIT
package/bin/cli.js CHANGED
@@ -1,59 +1,196 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
- * Requires the necessary modules for the script to run.
4
- * @requires ../lib/llm.js - provides the getComments function
5
- * @requires ../lib/config.js - provides the getConfig function
6
- * @requires fs - provides file system functionality
4
+ * Import required modules.
5
+ * @module llm
6
+ * @module config
7
+ * @module fs
8
+ * @module path
9
+ * @module readline
7
10
  */
8
- const { getComments } = require('../lib/llm.js')
9
- const { getConfig } = require('../lib/config.js')
11
+ const { getComments } = require('../lib/llm.js');
12
+ const { getConfig } = require('../lib/config.js');
10
13
  const fs = require('fs');
14
+ const path = require('path');
15
+ const readline = require('readline');
16
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
17
+
18
+ /**
19
+ * Asks a question and returns the answer as a promise.
20
+ * @param {string} query - The question to be asked.
21
+ * @returns {Promise<string>} The answer to the question.
22
+ */
23
+ const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
11
24
 
12
25
  /**
13
- * Retrieves the file path from the command line arguments.
26
+ * Get the filepath and arguments from the command line.
14
27
  * @type {string}
15
28
  */
16
- const filepath = process.argv[2]
29
+ const filepath = process.argv[2];
30
+ const args = process.argv.slice(3);
31
+ let mode = 'default';
17
32
 
18
33
  /**
19
- * Checks if a file path was provided.
20
- * If not, it logs the usage message and exits the process.
34
+ * Determine the mode based on the command line arguments.
35
+ * Supported modes are: 'light', 'full', and 'clean'.
36
+ */
37
+ if (args.includes('--light')) mode = 'light';
38
+ if (args.includes('--full')) mode = 'full';
39
+ if (args.includes('--clean')) mode = 'clean';
40
+ const isDryRun = args.includes('--dry-run');
41
+
42
+ /**
43
+ * Check if a filepath was provided, if not display usage information.
21
44
  */
22
45
  if (!filepath) {
23
- console.log("usage: commie <file>")
24
- process.exit(1)
25
- }
46
+ console.log("usage: devsplain <file-or-directory>");
47
+ process.exit(1);
48
+ }
49
+ else if (!fs.existsSync(filepath)) {
50
+ console.log(`Error: The path '${filepath}' does not exist.`);
51
+ process.exit(1);
52
+ }
26
53
  else {
27
- /**
28
- * Defines an asynchronous function to comment the code in the provided file.
29
- * @async
30
- */
31
54
  (async () => {
32
55
  /**
33
- * Retrieves the configuration settings.
56
+ * Get the configuration.
34
57
  * @type {object}
35
58
  */
36
59
  const config = await getConfig();
37
-
60
+
38
61
  /**
39
- * Reads the contents of the file at the provided file path.
40
- * @type {string}
62
+ * Process a path, either a file or directory.
63
+ * @param {string} targetPath - The path to process.
64
+ * @returns {Promise<void>}
41
65
  */
42
- const data = fs.readFileSync(filepath, 'utf-8');
43
-
44
- // Log a message to indicate that the file is being analyzed
45
- console.log("Analyzing File...");
46
-
66
+ async function processPath(targetPath) {
67
+ /**
68
+ * Get the stats of the target path.
69
+ * @type {fs.Stats}
70
+ */
71
+ const stats = fs.statSync(targetPath);
72
+
73
+ /**
74
+ * If the target path is a directory, process its contents.
75
+ */
76
+ if (stats.isDirectory()) {
77
+ const folderName = path.basename(targetPath);
78
+ /**
79
+ * List of ignored folders.
80
+ * @type {string[]}
81
+ */
82
+ const ignoredFolders = [
83
+ 'node_modules', '.git', 'dist', 'build', 'out',
84
+ '.next', '.nuxt', '.svelte-kit',
85
+ 'venv', 'env', '.venv',
86
+ '.vscode', '.idea', 'coverage'
87
+ ];
88
+
89
+ /**
90
+ * Check if the folder should be ignored.
91
+ */
92
+ if (ignoredFolders.includes(folderName)) {
93
+ // Folder will be skipped
94
+ return;
95
+ }
96
+
97
+ console.log(`\n Scanning directory: ${targetPath}`);
98
+ /**
99
+ * Read the contents of the directory.
100
+ * @type {string[]}
101
+ */
102
+ const items = fs.readdirSync(targetPath);
103
+
104
+ /**
105
+ * Process each item in the directory.
106
+ */
107
+ for (const item of items) {
108
+ const fullPath = path.join(targetPath, item);
109
+ await processPath(fullPath);
110
+ }
111
+ }
112
+ /**
113
+ * If the target path is a file, process it.
114
+ */
115
+ else if (stats.isFile()) {
116
+ /**
117
+ * Get the file extension.
118
+ * @type {string}
119
+ */
120
+ const ext = path.extname(targetPath).toLowerCase();
121
+ /**
122
+ * List of valid file extensions.
123
+ * @type {string[]}
124
+ */
125
+ const validExtensions = [
126
+ '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
127
+ '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
128
+ '.swift', '.kt', '.dart', '.sh'
129
+ ];
130
+
131
+ /**
132
+ * Check if the file extension is valid.
133
+ */
134
+ if (!validExtensions.includes(ext)) {
135
+ // File type is not supported, skipping
136
+ return;
137
+ }
138
+
139
+ const filename = path.basename(targetPath);
140
+ /**
141
+ * Read the file contents.
142
+ * @type {string}
143
+ */
144
+ const data = fs.readFileSync(targetPath, 'utf-8');
145
+
146
+ /**
147
+ * Check if the file is empty.
148
+ */
149
+ if (data.trim() === '') {
150
+ console.log(` Skipping ${filename} (Empty File)`);
151
+ return;
152
+ }
153
+
154
+ console.log(` Analyzing ${filename} in ${mode} mode...`);
155
+ /**
156
+ * Get the commented code.
157
+ * @type {string}
158
+ */
159
+ const commentedCode = await getComments(data, filename, config, mode);
160
+
161
+ /**
162
+ * If in dry run mode, display a preview of the commented code.
163
+ */
164
+ if (isDryRun) {
165
+ console.log(`\n --- DRY RUN PREVIEW: ${filename} ---`);
166
+ console.log(commentedCode);
167
+ console.log(`---------------------------------------\n`);
168
+
169
+ /**
170
+ * Ask the user if they want to save the commented code.
171
+ */
172
+ const answer = await askQuestion("Type 'write' to save to file, or press any key to discard ");
173
+
174
+ if (answer.toLowerCase() == 'write') {
175
+ fs.writeFileSync(targetPath, commentedCode);
176
+ console.log(` Successfully saved ${targetPath}`);
177
+ } else {
178
+ console.log(` Skipped ${targetPath}`);
179
+ }
180
+ } else {
181
+ /**
182
+ * Write the commented code to the file.
183
+ */
184
+ fs.writeFileSync(targetPath, commentedCode);
185
+ console.log(` Successfully commented ${targetPath}`);
186
+ }
187
+ }
188
+ }
189
+
47
190
  /**
48
- * Retrieves the commented code using the getComments function.
49
- * @type {string}
191
+ * Start processing the provided filepath.
50
192
  */
51
- const commentedCode = await getComments(data, 'javascript', config)
52
-
53
- // Write the commented code back to the file
54
- fs.writeFileSync(filepath, commentedCode);
55
-
56
- // Log a success message with the file path
57
- console.log(`Successfully commented ${filepath}`);
193
+ await processPath(filepath);
194
+ console.log("\n All done!");
58
195
  })();
59
196
  }
package/lib/config.js CHANGED
@@ -1,36 +1,81 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const readline = require('readline');
5
- const configPath = path.join(os.homedir(), '.commierc');
6
-
7
- const rl=readline.createInterface({
8
- input:process.stdin,
9
- output:process.stdout
10
- });
11
- const askQuestion=(query)=>new Promise((resolve)=>rl.question(query,resolve))
12
-
13
-
14
-
15
- async function getConfig(){
16
- if(!fs.existsSync(configPath)){
17
- console.log("Please Enter your api key");
18
- const answer=await askQuestion("Paste it here: ");
19
- rl.close()
20
- const config = {
21
- provider: 'groq',
22
- apiKey: answer,
23
- model: 'llama-3.3-70b-versatile',
24
- baseUrl: 'https://api.groq.com/openai'
25
- };
26
- fs.writeFileSync(configPath,JSON.stringify(config, null,2))
27
- return config
28
- }
29
- else {
30
- rl.close();
31
- const rawData = fs.readFileSync(configPath, 'utf8');
32
- return JSON.parse(rawData);
33
- }
34
- }
35
-
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const readline = require('readline');
5
+ const configPath = path.join(os.homedir(), '.devsplainrc');
6
+
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout
10
+ });
11
+
12
+ /**
13
+ * Asks a question via the readline interface.
14
+ * @param {string} query
15
+ * @returns {Promise<string>}
16
+ */
17
+ const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
18
+
19
+ /**
20
+ * Retrieves the configuration, creating a new one if it does not exist.
21
+ * @returns {Promise<Object>}
22
+ */
23
+ async function getConfig() {
24
+ if (!fs.existsSync(configPath)) {
25
+ let baseUrl = "";
26
+ let model = "";
27
+ let provider = "";
28
+
29
+ console.log("Which AI Provider Do You want to use?");
30
+ console.log("1. Groq (Free, Fast, Llama-3)");
31
+ console.log("2. Gemini (Free Tier)");
32
+ console.log("3. OpenAI (Paid)");
33
+ console.log("4. Custom (Ollama, local, etc)");
34
+
35
+ const choice = await askQuestion("Select (1-4): ");
36
+
37
+ if (choice === '1') {
38
+ provider = 'groq';
39
+ model = 'llama-3.3-70b-versatile';
40
+ baseUrl = 'https://api.groq.com/openai';
41
+ console.log("\nGet your free Groq key here: https://console.groq.com/keys");
42
+ } else if (choice === '2') {
43
+ provider = 'gemini';
44
+ model = 'gemini-2.0-flash';
45
+ baseUrl = null;
46
+ console.log("\nGet your free Gemini key here: https://aistudio.google.com/apikey");
47
+ } else if (choice === '3') {
48
+ provider = 'openai';
49
+ model = 'gpt-4o';
50
+ baseUrl = 'https://api.openai.com';
51
+ console.log("\nGet your OpenAI key here: https://platform.openai.com/api-keys");
52
+ } else if (choice === '4') {
53
+ provider = 'custom';
54
+ model = await askQuestion("Model name (e.g., llama3): ");
55
+ baseUrl = await askQuestion("Base URL (e.g., http://localhost:11434): ");
56
+ }
57
+
58
+ const apiKey = await askQuestion("Paste your API key (leave blank for local models): ");
59
+
60
+ rl.close();
61
+
62
+ const config = {
63
+ provider: provider,
64
+ apiKey: apiKey,
65
+ model: model,
66
+ baseUrl: baseUrl
67
+ };
68
+
69
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
70
+
71
+ return config;
72
+ } else {
73
+ rl.close();
74
+
75
+ const rawData = fs.readFileSync(configPath, 'utf8');
76
+
77
+ return JSON.parse(rawData);
78
+ }
79
+ }
80
+
36
81
  module.exports = { getConfig };
package/lib/llm.js CHANGED
@@ -1,53 +1,140 @@
1
- async function getComments(code,language,config){
2
- const prompt=
3
- `
4
- Add comments to this ${language} code. add JSDoc/docstrings block above functions and brief inline comments for complex logic. Return ONLY the commented code. NO MARKDOWN. NO EXPLANATION. NO PERSONAL MESSAGE.
5
- ${code}
6
- `
7
-
8
- if(config.provider==='gemini'){
9
- const url=`https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
10
- const response = await fetch(url,
11
- {
12
- method:'POST',
13
- headers:{'Content-Type':'application/json'},
14
- body:JSON.stringify({
15
- "contents":[{"parts":[{"text":prompt}]}]
16
- })
17
- }
18
- );
19
- const data=await response.json();
20
- console.log("DEBUG API RESPONSE:", data);
21
- if (data.error) {
22
- console.error("\n API Error:", data.error.message);
23
- process.exit(1);
24
- }
25
- let text = data.candidates[0].content.parts[0].text;
26
- text = text.replace(/^```[\w]*\n/m, '').replace(/```$/m, '').trim();
27
- return text;
28
- }
29
- else{
30
- const url=`${config.baseUrl}/v1/chat/completions`;
31
- const response=await fetch(url,{
32
- method:'POST',
33
- headers:{'Content-Type':'application/json','Authorization':`Bearer ${config.apiKey}`},
34
- body:JSON.stringify({ "model": config.model, "messages": [{ "role": "user", "content": prompt }]
35
- })
36
-
37
- })
38
- const data = await response.json();
39
- console.log("DEBUG API RESPONSE:", data);
40
- if (data.error) {
41
- console.error("\n API Error:", data.error.message);
42
- process.exit(1);
43
- }
44
- let text = data.choices[0].message.content;
45
- text = text.replace(/^```[\w]*\n/m, '').replace(/```$/m, '').trim();
46
- return text;
47
-
48
-
49
- }
50
-
51
- }
52
-
53
- module.exports={getComments};
1
+ /**
2
+ * This function generates commented code based on the given parameters.
3
+ * It sends a request to the specified AI provider with the provided code, language, and configuration.
4
+ * The function returns the commented code as a string.
5
+ *
6
+ * @param {string} code - The code that needs to be commented.
7
+ * @param {string} language - The programming language of the code.
8
+ * @param {object} config - The configuration object containing the provider and API details.
9
+ * @param {string} [mode='default'] - The mode of commenting. Default modes are 'default', 'light', 'full', and 'clean'.
10
+ * @returns {Promise<string>} The commented code.
11
+ */
12
+ async function getComments(code, language, config, mode = 'default') {
13
+ // Initialize the instruction based on the mode
14
+ let instruction = "Add JSDoc/docstrings block above functions. Do NOT add inline comments unless the logic is extremely complex or highly non-obvious. Keep the code clean and readable.";
15
+
16
+ // Check if the mode is 'light' and update the instruction accordingly
17
+ if (mode === 'light') {
18
+ // In 'light' mode, only JSDoc comments are added above functions
19
+ instruction = "Add ONLY JSDoc/docstrings above functions. Do NOT add any inline comments inside the functions. Keep it extremely minimal.";
20
+ }
21
+ // Check if the mode is 'full' and update the instruction accordingly
22
+ else if (mode === 'full') {
23
+ // In 'full' mode, detailed JSDoc comments and inline comments are added
24
+ instruction = "Add highly detailed JSDoc/docstrings above functions, and add detailed inline comments explaining almost every single line of logic. Do NOT add comments inside string literals, template literals, or multiline strings.";
25
+ }
26
+ // Check if the mode is 'clean' and update the instruction accordingly
27
+ else if (mode === 'clean') {
28
+ // In 'clean' mode, all comments are removed from the code
29
+ instruction = "Remove ALL comments (both block/JSDoc and inline comments) from this code. Return only the raw, uncommented code. Do NOT alter the code logic or formatting.";
30
+ }
31
+
32
+ // Initialize the prompt based on the mode
33
+ let prompt = "";
34
+ // Check if the mode is 'clean' and update the prompt accordingly
35
+ if (mode === 'clean') {
36
+ // In 'clean' mode, the prompt asks to remove all comments
37
+ prompt = `
38
+ Remove ALL comments (both block/JSDoc and inline comments) from the following ${language} code.
39
+ Return ONLY the raw, uncommented code. NO MARKDOWN. NO EXPLANATION. NO PERSONAL MESSAGE.
40
+ Do NOT alter the code logic or formatting in any way.
41
+ ${code}
42
+ `;
43
+ } else {
44
+ // In other modes, the prompt asks to add comments to the code
45
+ prompt = `
46
+ Add comments to the code in this file (${language}). ${instruction}
47
+ If the code already contains comments, completely REPLACE them with your own. Do not leave duplicate or messy comments behind.
48
+ Return ONLY the commented code. NO MARKDOWN. NO EXPLANATION. NO PERSONAL MESSAGE.
49
+ Do NOT alter, refactor, or format the existing code in any way. Only add comments. IF ANYTHING WRONG HIGHLIGHT IN COMMENT BUT DO NOT CHANGE
50
+ ${code}
51
+ `;
52
+ }
53
+
54
+ // Check if the provider is 'gemini' and proceed accordingly
55
+ if (config.provider === 'gemini') {
56
+ // Construct the URL for the API request
57
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
58
+ let data;
59
+
60
+ try {
61
+ // Send a POST request to the API with the prompt
62
+ const response = await fetch(url, {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json'
66
+ },
67
+ body: JSON.stringify({
68
+ "contents": [{ "parts": [{ "text": prompt }] }]
69
+ })
70
+ });
71
+ // Parse the response as JSON
72
+ data = await response.json();
73
+ } catch (error) {
74
+ // Log any network errors and exit the process
75
+ console.log("\n Network Error: Could not connect to the AI provider. Check your internet or API url.");
76
+ process.exit(1);
77
+ }
78
+
79
+ // Check if there is an error in the API response
80
+ if (data.error) {
81
+ // Log the API error and exit the process
82
+ console.error("\n API Error:", data.error.message);
83
+ process.exit(1);
84
+ }
85
+
86
+ // Extract the generated text from the API response
87
+ let text = data.candidates[0].content.parts[0].text;
88
+ // Remove any unnecessary code blocks and trim the text
89
+ text = text.replace(/^```[\w]*\n/m, '').replace(/```$/m, '').trim();
90
+ // Return the generated text
91
+ return text;
92
+ }
93
+ // If the provider is not 'gemini', proceed with the default API request
94
+ else {
95
+ // Construct the URL for the API request
96
+ const url = `${config.baseUrl}/v1/chat/completions`;
97
+ let data;
98
+
99
+ try {
100
+ // Send a POST request to the API with the prompt
101
+ const response = await fetch(url, {
102
+ method: 'POST',
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ 'Authorization': `Bearer ${config.apiKey}`
106
+ },
107
+ body: JSON.stringify({
108
+ "model": config.model,
109
+ "messages": [{
110
+ "role": "user",
111
+ "content": prompt
112
+ }]
113
+ })
114
+ });
115
+ // Parse the response as JSON
116
+ data = await response.json();
117
+ } catch (error) {
118
+ // Log any network errors and exit the process
119
+ console.log("\n Network Error: Could not connect to the AI provider. Check your internet or API url.");
120
+ process.exit(1);
121
+ }
122
+
123
+ // Check if there is an error in the API response
124
+ if (data.error) {
125
+ // Log the API error and exit the process
126
+ console.error("\n API Error:", data.error.message);
127
+ process.exit(1);
128
+ }
129
+
130
+ // Extract the generated text from the API response
131
+ let text = data.choices[0].message.content;
132
+ // Remove any unnecessary code blocks and trim the text
133
+ text = text.replace(/^```[\w]*\n/m, '').replace(/```$/m, '').trim();
134
+ // Return the generated text
135
+ return text;
136
+ }
137
+ }
138
+
139
+ // Export the getComments function as a module
140
+ module.exports = { getComments };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "An agent-agnostic CLI tool that automatically adds JSDoc and inline comments to your code using free LLMs.",
5
5
  "author": "mwahaj36",
6
6
  "license": "MIT",
@@ -28,13 +28,16 @@
28
28
  ],
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "git+https://github.com/mwahaj36/commie.git"
31
+ "url": "git+https://github.com/mwahaj36/devsplain.git"
32
32
  },
33
33
  "bugs": {
34
- "url": "https://github.com/mwahaj36/commie/issues"
34
+ "url": "https://github.com/mwahaj36/devsplain/issues"
35
35
  },
36
- "homepage": "https://github.com/mwahaj36/commie#readme",
36
+ "homepage": "https://github.com/mwahaj36/devsplain#readme",
37
37
  "scripts": {
38
- "test": "echo \"Error: no test specified\" && exit 1"
38
+ "test": "jest"
39
+ },
40
+ "devDependencies": {
41
+ "jest": "^30.4.2"
39
42
  }
40
43
  }