@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 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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ import '../dist/cli.mjs'
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
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.d.mts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
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
+ }