@snelusha/noto 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/LICENSE +21 -0
- package/README.md +36 -0
- package/bin/noto.mjs +4 -0
- package/dist/cli.cjs +171 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.mjs +161 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Sithija Nelusha Silva<https://github.com/snelusha>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<h1 align="center">✨ Noto</h1>
|
|
2
|
+
<p align="center"><sup>(/nōto/, <em>notebook</em> in Japanese)</sup></p>
|
|
3
|
+
<p align="center">Generate clean commit messages in a snap! ✨</p>
|
|
4
|
+
|
|
5
|
+
<pre align="center">
|
|
6
|
+
<p>npm install -g <b>@snelusha/noto</b></p>
|
|
7
|
+
</pre>
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Customizable:** Provide your own Google GenAI API Key for personalized experience.
|
|
12
|
+
- **No Installation Required:** Use instantly with `npx @snelusha/noto` — just run and go!
|
|
13
|
+
|
|
14
|
+
## Getting Started
|
|
15
|
+
|
|
16
|
+
1. **Configuration:**
|
|
17
|
+
Before diving in, run the following command to configure your `noto`:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
noto config
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Here, you’ll need to input your Google GenAI API Key.
|
|
24
|
+
|
|
25
|
+
2. **Generate commit messages**
|
|
26
|
+
Simply run:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
noto
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Thank you for using `noto`! If you have any feedback or suggestions, feel free to reach out or contribute to the project. ✨
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
This project is licensed under the MIT License.
|
|
36
|
+
© 2024 [Sithija Nelusha Silva](https://github.com/snelusha)
|
package/bin/noto.mjs
ADDED
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const yargs = require('yargs');
|
|
4
|
+
const prompts = require('@posva/prompts');
|
|
5
|
+
const c = require('picocolors');
|
|
6
|
+
const helpers = require('yargs/helpers');
|
|
7
|
+
const node_path = require('node:path');
|
|
8
|
+
const node_fs = require('node:fs');
|
|
9
|
+
const os = require('node:os');
|
|
10
|
+
const process$1 = require('node:process');
|
|
11
|
+
require('which');
|
|
12
|
+
const tinyexec = require('tinyexec');
|
|
13
|
+
const ai = require('ai');
|
|
14
|
+
const google = require('@ai-sdk/google');
|
|
15
|
+
|
|
16
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
17
|
+
|
|
18
|
+
const yargs__default = /*#__PURE__*/_interopDefaultCompat(yargs);
|
|
19
|
+
const prompts__default = /*#__PURE__*/_interopDefaultCompat(prompts);
|
|
20
|
+
const c__default = /*#__PURE__*/_interopDefaultCompat(c);
|
|
21
|
+
const os__default = /*#__PURE__*/_interopDefaultCompat(os);
|
|
22
|
+
const process__default = /*#__PURE__*/_interopDefaultCompat(process$1);
|
|
23
|
+
|
|
24
|
+
const CLI_TEMP_DIR = node_path.join(os__default.tmpdir(), "snelusha-noto");
|
|
25
|
+
let counter = 0;
|
|
26
|
+
async function openTemp() {
|
|
27
|
+
if (!node_fs.existsSync(CLI_TEMP_DIR))
|
|
28
|
+
await node_fs.promises.mkdir(CLI_TEMP_DIR, { recursive: true });
|
|
29
|
+
const competitivePath = node_path.join(CLI_TEMP_DIR, `.${process__default.pid}.${counter}`);
|
|
30
|
+
counter += 1;
|
|
31
|
+
return node_fs.promises.open(competitivePath, "wx").then((fd) => ({
|
|
32
|
+
fd,
|
|
33
|
+
path: competitivePath,
|
|
34
|
+
cleanup() {
|
|
35
|
+
fd.close().then(() => {
|
|
36
|
+
if (node_fs.existsSync(competitivePath))
|
|
37
|
+
node_fs.promises.unlink(competitivePath);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
})).catch((error) => {
|
|
41
|
+
if (error && error.code === "EEXIST")
|
|
42
|
+
return openTemp();
|
|
43
|
+
else
|
|
44
|
+
return void 0;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function writeFileSafe(path, data = "") {
|
|
48
|
+
const temp = await openTemp();
|
|
49
|
+
if (temp) {
|
|
50
|
+
try {
|
|
51
|
+
await node_fs.promises.writeFile(temp.path, data);
|
|
52
|
+
const directory = node_path.dirname(path);
|
|
53
|
+
if (!node_fs.existsSync(directory))
|
|
54
|
+
await node_fs.promises.mkdir(directory, { recursive: true });
|
|
55
|
+
await node_fs.promises.rename(temp.path, path);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
} finally {
|
|
60
|
+
temp.cleanup();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let storage;
|
|
67
|
+
const storagePath = node_path.resolve(CLI_TEMP_DIR, "storage.json");
|
|
68
|
+
async function load(fn) {
|
|
69
|
+
if (!storage) {
|
|
70
|
+
storage = node_fs.existsSync(storagePath) ? JSON.parse(await node_fs.promises.readFile(storagePath, "utf-8") || "{}") || {} : {};
|
|
71
|
+
}
|
|
72
|
+
if (fn) {
|
|
73
|
+
if (await fn(storage))
|
|
74
|
+
await dump();
|
|
75
|
+
}
|
|
76
|
+
return storage;
|
|
77
|
+
}
|
|
78
|
+
async function dump() {
|
|
79
|
+
if (storage)
|
|
80
|
+
await writeFileSafe(storagePath, JSON.stringify(storage));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isGitRepository(path) {
|
|
84
|
+
return node_fs.existsSync(node_path.join(path, ".git"));
|
|
85
|
+
}
|
|
86
|
+
async function getStagedDiff() {
|
|
87
|
+
try {
|
|
88
|
+
const fullDiff = (await tinyexec.x("git", ["diff", "--cached"])).stdout.toString();
|
|
89
|
+
return fullDiff;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function generateCommitMessage(diff) {
|
|
96
|
+
const storage = await load();
|
|
97
|
+
const google$1 = google.createGoogleGenerativeAI({
|
|
98
|
+
apiKey: storage.apiKey
|
|
99
|
+
});
|
|
100
|
+
const message = await ai.generateText({
|
|
101
|
+
model: google$1("gemini-1.5-flash-latest"),
|
|
102
|
+
messages: [
|
|
103
|
+
{
|
|
104
|
+
role: "system",
|
|
105
|
+
content: "You are tasked with generating a Git commit message based on the following staged changes across multiple files. Identify only the key changes and generate a concise commit message in the format: <type>: <description>. The commit message must be a single line, in all lowercase, without a scope or body, and no full stops at the end. Use one of these types: feat, fix, refactor, docs, test, or chore. Do not mention file names unless a file was renamed or is essential to understanding the change. Focus on the most important change, and the message must not exceed 72 characters."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
role: "user",
|
|
109
|
+
content: `generated commit message for the following staged changes:
|
|
110
|
+
${diff}`
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
});
|
|
114
|
+
return message.text.trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
yargs__default(helpers.hideBin(process.argv)).scriptName("noto").usage("$0 [args]").command(
|
|
118
|
+
"config",
|
|
119
|
+
"configure",
|
|
120
|
+
() => {
|
|
121
|
+
},
|
|
122
|
+
async () => {
|
|
123
|
+
const storage = await load();
|
|
124
|
+
if (storage.apiKey) {
|
|
125
|
+
const response2 = await prompts__default({
|
|
126
|
+
type: "confirm",
|
|
127
|
+
name: "reset",
|
|
128
|
+
message: "Do you want to reset your API key?"
|
|
129
|
+
});
|
|
130
|
+
if (!response2.reset) {
|
|
131
|
+
console.log(`Use ${c__default.bold("`noto`")} to generate commit message!`);
|
|
132
|
+
return process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const response = await prompts__default({
|
|
136
|
+
type: "text",
|
|
137
|
+
name: "apiKey",
|
|
138
|
+
message: "Enter your API key:",
|
|
139
|
+
validate: (value) => value ? true : "API key is required"
|
|
140
|
+
});
|
|
141
|
+
storage.apiKey = response.apiKey;
|
|
142
|
+
dump();
|
|
143
|
+
console.log(`Use ${c__default.bold("`noto`")} to generate commit message!`);
|
|
144
|
+
}
|
|
145
|
+
).command(
|
|
146
|
+
"*",
|
|
147
|
+
"generate commit message",
|
|
148
|
+
() => {
|
|
149
|
+
},
|
|
150
|
+
async () => {
|
|
151
|
+
const storage = await load();
|
|
152
|
+
if (!storage.apiKey) {
|
|
153
|
+
console.log(
|
|
154
|
+
`Please run ${c__default.bold("`noto config`")} to set your API key.`
|
|
155
|
+
);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const cwd = process.cwd();
|
|
159
|
+
if (!isGitRepository(cwd)) {
|
|
160
|
+
console.log(c__default.red("hmm! git repository not found"));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
const diff = await getStagedDiff();
|
|
164
|
+
if (!diff) {
|
|
165
|
+
console.log(c__default.red("hmm! no staged diff found"));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
const message = await generateCommitMessage(diff);
|
|
169
|
+
console.log(c__default.white(message));
|
|
170
|
+
}
|
|
171
|
+
).alias("-v", "--version").alias("-h", "--help").argv;
|
package/dist/cli.d.cts
ADDED
package/dist/cli.d.mts
ADDED
package/dist/cli.d.ts
ADDED
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import prompts from '@posva/prompts';
|
|
3
|
+
import c from 'picocolors';
|
|
4
|
+
import { hideBin } from 'yargs/helpers';
|
|
5
|
+
import { join, dirname, resolve } from 'node:path';
|
|
6
|
+
import { promises, existsSync } from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import process$1 from 'node:process';
|
|
9
|
+
import 'which';
|
|
10
|
+
import { x } from 'tinyexec';
|
|
11
|
+
import { generateText } from 'ai';
|
|
12
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
13
|
+
|
|
14
|
+
const CLI_TEMP_DIR = join(os.tmpdir(), "snelusha-noto");
|
|
15
|
+
let counter = 0;
|
|
16
|
+
async function openTemp() {
|
|
17
|
+
if (!existsSync(CLI_TEMP_DIR))
|
|
18
|
+
await promises.mkdir(CLI_TEMP_DIR, { recursive: true });
|
|
19
|
+
const competitivePath = join(CLI_TEMP_DIR, `.${process$1.pid}.${counter}`);
|
|
20
|
+
counter += 1;
|
|
21
|
+
return promises.open(competitivePath, "wx").then((fd) => ({
|
|
22
|
+
fd,
|
|
23
|
+
path: competitivePath,
|
|
24
|
+
cleanup() {
|
|
25
|
+
fd.close().then(() => {
|
|
26
|
+
if (existsSync(competitivePath))
|
|
27
|
+
promises.unlink(competitivePath);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
})).catch((error) => {
|
|
31
|
+
if (error && error.code === "EEXIST")
|
|
32
|
+
return openTemp();
|
|
33
|
+
else
|
|
34
|
+
return void 0;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function writeFileSafe(path, data = "") {
|
|
38
|
+
const temp = await openTemp();
|
|
39
|
+
if (temp) {
|
|
40
|
+
try {
|
|
41
|
+
await promises.writeFile(temp.path, data);
|
|
42
|
+
const directory = dirname(path);
|
|
43
|
+
if (!existsSync(directory))
|
|
44
|
+
await promises.mkdir(directory, { recursive: true });
|
|
45
|
+
await promises.rename(temp.path, path);
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
} finally {
|
|
50
|
+
temp.cleanup();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let storage;
|
|
57
|
+
const storagePath = resolve(CLI_TEMP_DIR, "storage.json");
|
|
58
|
+
async function load(fn) {
|
|
59
|
+
if (!storage) {
|
|
60
|
+
storage = existsSync(storagePath) ? JSON.parse(await promises.readFile(storagePath, "utf-8") || "{}") || {} : {};
|
|
61
|
+
}
|
|
62
|
+
if (fn) {
|
|
63
|
+
if (await fn(storage))
|
|
64
|
+
await dump();
|
|
65
|
+
}
|
|
66
|
+
return storage;
|
|
67
|
+
}
|
|
68
|
+
async function dump() {
|
|
69
|
+
if (storage)
|
|
70
|
+
await writeFileSafe(storagePath, JSON.stringify(storage));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isGitRepository(path) {
|
|
74
|
+
return existsSync(join(path, ".git"));
|
|
75
|
+
}
|
|
76
|
+
async function getStagedDiff() {
|
|
77
|
+
try {
|
|
78
|
+
const fullDiff = (await x("git", ["diff", "--cached"])).stdout.toString();
|
|
79
|
+
return fullDiff;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function generateCommitMessage(diff) {
|
|
86
|
+
const storage = await load();
|
|
87
|
+
const google = createGoogleGenerativeAI({
|
|
88
|
+
apiKey: storage.apiKey
|
|
89
|
+
});
|
|
90
|
+
const message = await generateText({
|
|
91
|
+
model: google("gemini-1.5-flash-latest"),
|
|
92
|
+
messages: [
|
|
93
|
+
{
|
|
94
|
+
role: "system",
|
|
95
|
+
content: "You are tasked with generating a Git commit message based on the following staged changes across multiple files. Identify only the key changes and generate a concise commit message in the format: <type>: <description>. The commit message must be a single line, in all lowercase, without a scope or body, and no full stops at the end. Use one of these types: feat, fix, refactor, docs, test, or chore. Do not mention file names unless a file was renamed or is essential to understanding the change. Focus on the most important change, and the message must not exceed 72 characters."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
role: "user",
|
|
99
|
+
content: `generated commit message for the following staged changes:
|
|
100
|
+
${diff}`
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
return message.text.trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
yargs(hideBin(process.argv)).scriptName("noto").usage("$0 [args]").command(
|
|
108
|
+
"config",
|
|
109
|
+
"configure",
|
|
110
|
+
() => {
|
|
111
|
+
},
|
|
112
|
+
async () => {
|
|
113
|
+
const storage = await load();
|
|
114
|
+
if (storage.apiKey) {
|
|
115
|
+
const response2 = await prompts({
|
|
116
|
+
type: "confirm",
|
|
117
|
+
name: "reset",
|
|
118
|
+
message: "Do you want to reset your API key?"
|
|
119
|
+
});
|
|
120
|
+
if (!response2.reset) {
|
|
121
|
+
console.log(`Use ${c.bold("`noto`")} to generate commit message!`);
|
|
122
|
+
return process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const response = await prompts({
|
|
126
|
+
type: "text",
|
|
127
|
+
name: "apiKey",
|
|
128
|
+
message: "Enter your API key:",
|
|
129
|
+
validate: (value) => value ? true : "API key is required"
|
|
130
|
+
});
|
|
131
|
+
storage.apiKey = response.apiKey;
|
|
132
|
+
dump();
|
|
133
|
+
console.log(`Use ${c.bold("`noto`")} to generate commit message!`);
|
|
134
|
+
}
|
|
135
|
+
).command(
|
|
136
|
+
"*",
|
|
137
|
+
"generate commit message",
|
|
138
|
+
() => {
|
|
139
|
+
},
|
|
140
|
+
async () => {
|
|
141
|
+
const storage = await load();
|
|
142
|
+
if (!storage.apiKey) {
|
|
143
|
+
console.log(
|
|
144
|
+
`Please run ${c.bold("`noto config`")} to set your API key.`
|
|
145
|
+
);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const cwd = process.cwd();
|
|
149
|
+
if (!isGitRepository(cwd)) {
|
|
150
|
+
console.log(c.red("hmm! git repository not found"));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
const diff = await getStagedDiff();
|
|
154
|
+
if (!diff) {
|
|
155
|
+
console.log(c.red("hmm! no staged diff found"));
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const message = await generateCommitMessage(diff);
|
|
159
|
+
console.log(c.white(message));
|
|
160
|
+
}
|
|
161
|
+
).alias("-v", "--version").alias("-h", "--help").argv;
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@snelusha/noto",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "generate clean commit messages in a snap! ✨",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Sithija Nelusha Silva",
|
|
9
|
+
"email": "hello@snelusha.dev",
|
|
10
|
+
"url": "https://snelusha.dev"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/snelusha/noto",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/snelusha/noto.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/snelusha/noto/issues"
|
|
19
|
+
},
|
|
20
|
+
"main": "dist/cli.mjs",
|
|
21
|
+
"module": "dist/cli.mjs",
|
|
22
|
+
"types": "dist/cli.d.ts",
|
|
23
|
+
"bin": {
|
|
24
|
+
"noto": "bin/noto.mjs"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "unbuild",
|
|
31
|
+
"start": "esno cli.ts"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/bun": "latest",
|
|
35
|
+
"@types/which": "^3.0.4",
|
|
36
|
+
"@types/yargs": "^17.0.33",
|
|
37
|
+
"esno": "^4.8.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"typescript": "^5.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@ai-sdk/google": "^0.0.51",
|
|
44
|
+
"@posva/prompts": "^2.4.4",
|
|
45
|
+
"ai": "^3.4.9",
|
|
46
|
+
"picocolors": "^1.1.0",
|
|
47
|
+
"tinyexec": "^0.3.0",
|
|
48
|
+
"unbuild": "^2.0.0",
|
|
49
|
+
"which": "^5.0.0",
|
|
50
|
+
"yargs": "^17.7.2"
|
|
51
|
+
}
|
|
52
|
+
}
|