@snelusha/noto 0.1.0 → 0.2.1

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 CHANGED
@@ -8,7 +8,10 @@
8
8
 
9
9
  ## Features
10
10
 
11
- - **Customizable:** Provide your own Google GenAI API Key for personalized experience.
11
+ - **Instant Commit Messages**: Generate clear, context-aware messages based on staged changes.
12
+
13
+ - **Seamless Git Integration**: Apply messages directly, skip the copy-paste.
14
+
12
15
  - **No Installation Required:** Use instantly with `npx @snelusha/noto` — just run and go!
13
16
 
14
17
  ## Getting Started
@@ -23,14 +26,33 @@
23
26
  Here, you’ll need to input your Google GenAI API Key.
24
27
 
25
28
  2. **Generate commit messages**
26
- Simply run:
29
+ To generate a commit message, simply run:
30
+
31
+ ```bash
32
+ noto
33
+
34
+ # apply the generated message:
35
+ noto --apply
36
+ # or
37
+ noto -a
27
38
 
28
- ```bash
29
- noto
30
- ```
39
+ # copy the generated message:
40
+ noto --copy
41
+ # or
42
+ noto -c
43
+ ```
44
+
45
+ ## Pro Tips
46
+
47
+ - 🚀 Get fast commits on the fly with `noto -a` to streamline your workflow!
48
+
49
+ ## Contributing
50
+
51
+ We welcome contributions and suggestions! If you have ideas or improvements, feel free to reach out or open a pull request.
31
52
 
32
53
  Thank you for using `noto`! If you have any feedback or suggestions, feel free to reach out or contribute to the project. ✨
33
54
 
34
55
  ## License
56
+
35
57
  This project is licensed under the MIT License.
36
58
  © 2024 [Sithija Nelusha Silva](https://github.com/snelusha)
package/dist/cli.cjs CHANGED
@@ -3,12 +3,14 @@
3
3
  const yargs = require('yargs');
4
4
  const prompts = require('@posva/prompts');
5
5
  const c = require('picocolors');
6
+ const clipboardy = require('clipboardy');
6
7
  const helpers = require('yargs/helpers');
7
8
  const node_path = require('node:path');
8
9
  const node_fs = require('node:fs');
9
10
  const os = require('node:os');
10
11
  const process$1 = require('node:process');
11
12
  require('which');
13
+ const ora = require('ora');
12
14
  const tinyexec = require('tinyexec');
13
15
  const ai = require('ai');
14
16
  const google = require('@ai-sdk/google');
@@ -18,15 +20,17 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
18
20
  const yargs__default = /*#__PURE__*/_interopDefaultCompat(yargs);
19
21
  const prompts__default = /*#__PURE__*/_interopDefaultCompat(prompts);
20
22
  const c__default = /*#__PURE__*/_interopDefaultCompat(c);
23
+ const clipboardy__default = /*#__PURE__*/_interopDefaultCompat(clipboardy);
21
24
  const os__default = /*#__PURE__*/_interopDefaultCompat(os);
22
25
  const process__default = /*#__PURE__*/_interopDefaultCompat(process$1);
26
+ const ora__default = /*#__PURE__*/_interopDefaultCompat(ora);
23
27
 
24
- const CLI_TEMP_DIR = node_path.join(os__default.tmpdir(), "snelusha-noto");
28
+ const TEMP_DIR = node_path.join(os__default.tmpdir(), "snelusha-noto");
25
29
  let counter = 0;
26
30
  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}`);
31
+ if (!node_fs.existsSync(TEMP_DIR))
32
+ await node_fs.promises.mkdir(TEMP_DIR, { recursive: true });
33
+ const competitivePath = node_path.join(TEMP_DIR, `.${process__default.pid}.${counter}`);
30
34
  counter += 1;
31
35
  return node_fs.promises.open(competitivePath, "wx").then((fd) => ({
32
36
  fd,
@@ -62,22 +66,56 @@ async function writeFileSafe(path, data = "") {
62
66
  }
63
67
  return false;
64
68
  }
69
+ function spinner() {
70
+ let s;
71
+ return {
72
+ start(text) {
73
+ s = ora__default(text);
74
+ s.spinner = {
75
+ interval: 150,
76
+ frames: ["\u2736", "\u2738", "\u2739", "\u273A", "\u2739", "\u2737"]
77
+ };
78
+ s.start();
79
+ },
80
+ fail(text) {
81
+ if (s)
82
+ s.fail(text);
83
+ },
84
+ success(text) {
85
+ if (s)
86
+ s.succeed(text);
87
+ },
88
+ stop() {
89
+ if (s)
90
+ s.stop();
91
+ }
92
+ };
93
+ }
65
94
 
66
- let storage;
67
- const storagePath = node_path.resolve(CLI_TEMP_DIR, "storage.json");
95
+ let storage = {};
96
+ const storagePath = node_path.resolve(TEMP_DIR, "storage.json");
68
97
  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))
98
+ try {
99
+ if (!Object.keys(storage).length) {
100
+ storage = node_fs.existsSync(storagePath) ? JSON.parse(await node_fs.promises.readFile(storagePath, "utf-8") || "{}") || {} : {};
101
+ }
102
+ if (fn && await fn(storage)) {
74
103
  await dump();
104
+ }
105
+ } catch (error) {
106
+ console.error("error loading storage:", error);
107
+ storage = {};
75
108
  }
76
109
  return storage;
77
110
  }
78
111
  async function dump() {
79
- if (storage)
80
- await writeFileSafe(storagePath, JSON.stringify(storage));
112
+ try {
113
+ if (storage) {
114
+ await writeFileSafe(storagePath, JSON.stringify(storage, null, 2));
115
+ }
116
+ } catch (error) {
117
+ console.error("error saving storage:", error);
118
+ }
81
119
  }
82
120
 
83
121
  function isGitRepository(path) {
@@ -85,12 +123,20 @@ function isGitRepository(path) {
85
123
  }
86
124
  async function getStagedDiff() {
87
125
  try {
88
- const fullDiff = (await tinyexec.x("git", ["diff", "--cached"])).stdout.toString();
89
- return fullDiff;
126
+ const diff = (await tinyexec.x("git", ["diff", "--cached"])).stdout.toString();
127
+ return diff;
90
128
  } catch {
91
129
  return null;
92
130
  }
93
131
  }
132
+ async function commit(message) {
133
+ try {
134
+ const result = await tinyexec.x("git", ["commit", "-m", message]);
135
+ return /file(s)? changed/i.test(result.stdout.toString());
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
94
140
 
95
141
  async function generateCommitMessage(diff) {
96
142
  const storage = await load();
@@ -116,56 +162,113 @@ ${diff}`
116
162
 
117
163
  yargs__default(helpers.hideBin(process.argv)).scriptName("noto").usage("$0 [args]").command(
118
164
  "config",
119
- "configure",
165
+ "setup you API key to enable noto.",
120
166
  () => {
121
167
  },
122
168
  async () => {
123
169
  const storage = await load();
124
170
  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
- });
171
+ const response2 = await prompts__default(
172
+ {
173
+ type: "confirm",
174
+ name: "reset",
175
+ message: "Do you want to reset your API key?"
176
+ },
177
+ {
178
+ onCancel: () => process.exit(0)
179
+ }
180
+ );
130
181
  if (!response2.reset) {
131
- console.log(`Use ${c__default.bold("`noto`")} to generate commit message!`);
182
+ console.log(
183
+ `Use ${c__default.greenBright(
184
+ c__default.bold("`noto`")
185
+ )} to generate your commit message!`
186
+ );
132
187
  return process.exit(0);
133
188
  }
134
189
  }
135
190
  const response = await prompts__default({
136
- type: "text",
191
+ type: "password",
137
192
  name: "apiKey",
138
- message: "Enter your API key:",
139
- validate: (value) => value ? true : "API key is required"
193
+ message: "Please enter your API key:",
194
+ validate: (value) => value ? true : "API key is required!"
140
195
  });
141
- storage.apiKey = response.apiKey;
142
- dump();
143
- console.log(`Use ${c__default.bold("`noto`")} to generate commit message!`);
196
+ if (response.apiKey) {
197
+ storage.apiKey = response.apiKey;
198
+ dump();
199
+ console.log("API key saved successfully!");
200
+ console.log(
201
+ `Use ${c__default.greenBright(
202
+ c__default.bold("`noto`")
203
+ )} to generate your commit message!`
204
+ );
205
+ }
144
206
  }
145
207
  ).command(
146
208
  "*",
147
209
  "generate commit message",
148
- () => {
210
+ (args) => {
211
+ args.option("copy", {
212
+ alias: "c",
213
+ type: "boolean",
214
+ description: "Copy the generated commit message to the clipboard."
215
+ });
216
+ args.option("apply", {
217
+ alias: "a",
218
+ type: "boolean",
219
+ description: "Commit the staged changes with the generated message."
220
+ });
149
221
  },
150
- async () => {
222
+ async (args) => {
151
223
  const storage = await load();
152
224
  if (!storage.apiKey) {
153
225
  console.log(
154
- `Please run ${c__default.bold("`noto config`")} to set your API key.`
226
+ `Please run ${c__default.cyan(c__default.bold("`noto config`"))} to set your API key.`
155
227
  );
156
228
  process.exit(1);
157
229
  }
158
230
  const cwd = process.cwd();
159
231
  if (!isGitRepository(cwd)) {
160
- console.log(c__default.red("hmm! git repository not found"));
232
+ console.log(
233
+ c__default.red("Oops! No Git repository found in the current directory.")
234
+ );
235
+ console.log(
236
+ `You can initialize one by running ${c__default.cyan(c__default.bold("`git init`"))}`
237
+ );
161
238
  process.exit(1);
162
239
  }
163
240
  const diff = await getStagedDiff();
164
241
  if (!diff) {
165
- console.log(c__default.red("hmm! no staged diff found"));
242
+ console.log(c__default.red("Oops! No staged changes found to commit."));
243
+ console.log(
244
+ `Stage changes with ${c__default.cyan(c__default.bold("`git add <file>`"))} or ${c__default.cyan(
245
+ c__default.bold("`git add .`")
246
+ )} for stage all files.`
247
+ );
248
+ process.exit(1);
249
+ }
250
+ const spin = spinner();
251
+ try {
252
+ spin.start("Generating commit message...");
253
+ const message = await generateCommitMessage(diff);
254
+ const copy = args.copy;
255
+ const apply = args.apply;
256
+ spin.success(`Commit Message: ${c__default.dim(c__default.bold(message))}`);
257
+ if (copy) {
258
+ clipboardy__default.writeSync(message);
259
+ spin.success("Message copied to clipboard!");
260
+ }
261
+ if (apply) {
262
+ spin.start("Committing staged changes...");
263
+ if (!await commit(message)) {
264
+ spin.fail("Failed to commit staged changes.");
265
+ process.exit(1);
266
+ }
267
+ spin.success("Staged changes committed successfully!");
268
+ }
269
+ } catch (error) {
270
+ spin.fail("Failed to generate commit message.");
166
271
  process.exit(1);
167
272
  }
168
- const message = await generateCommitMessage(diff);
169
- console.log(c__default.white(message));
170
273
  }
171
- ).alias("-v", "--version").alias("-h", "--help").argv;
274
+ ).version().alias("-v", "--version").alias("-h", "--help").argv;
package/dist/cli.mjs CHANGED
@@ -1,22 +1,24 @@
1
1
  import yargs from 'yargs';
2
2
  import prompts from '@posva/prompts';
3
3
  import c from 'picocolors';
4
+ import clipboardy from 'clipboardy';
4
5
  import { hideBin } from 'yargs/helpers';
5
6
  import { join, dirname, resolve } from 'node:path';
6
7
  import { promises, existsSync } from 'node:fs';
7
8
  import os from 'node:os';
8
9
  import process$1 from 'node:process';
9
10
  import 'which';
11
+ import ora from 'ora';
10
12
  import { x } from 'tinyexec';
11
13
  import { generateText } from 'ai';
12
14
  import { createGoogleGenerativeAI } from '@ai-sdk/google';
13
15
 
14
- const CLI_TEMP_DIR = join(os.tmpdir(), "snelusha-noto");
16
+ const TEMP_DIR = join(os.tmpdir(), "snelusha-noto");
15
17
  let counter = 0;
16
18
  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}`);
19
+ if (!existsSync(TEMP_DIR))
20
+ await promises.mkdir(TEMP_DIR, { recursive: true });
21
+ const competitivePath = join(TEMP_DIR, `.${process$1.pid}.${counter}`);
20
22
  counter += 1;
21
23
  return promises.open(competitivePath, "wx").then((fd) => ({
22
24
  fd,
@@ -52,22 +54,56 @@ async function writeFileSafe(path, data = "") {
52
54
  }
53
55
  return false;
54
56
  }
57
+ function spinner() {
58
+ let s;
59
+ return {
60
+ start(text) {
61
+ s = ora(text);
62
+ s.spinner = {
63
+ interval: 150,
64
+ frames: ["\u2736", "\u2738", "\u2739", "\u273A", "\u2739", "\u2737"]
65
+ };
66
+ s.start();
67
+ },
68
+ fail(text) {
69
+ if (s)
70
+ s.fail(text);
71
+ },
72
+ success(text) {
73
+ if (s)
74
+ s.succeed(text);
75
+ },
76
+ stop() {
77
+ if (s)
78
+ s.stop();
79
+ }
80
+ };
81
+ }
55
82
 
56
- let storage;
57
- const storagePath = resolve(CLI_TEMP_DIR, "storage.json");
83
+ let storage = {};
84
+ const storagePath = resolve(TEMP_DIR, "storage.json");
58
85
  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))
86
+ try {
87
+ if (!Object.keys(storage).length) {
88
+ storage = existsSync(storagePath) ? JSON.parse(await promises.readFile(storagePath, "utf-8") || "{}") || {} : {};
89
+ }
90
+ if (fn && await fn(storage)) {
64
91
  await dump();
92
+ }
93
+ } catch (error) {
94
+ console.error("error loading storage:", error);
95
+ storage = {};
65
96
  }
66
97
  return storage;
67
98
  }
68
99
  async function dump() {
69
- if (storage)
70
- await writeFileSafe(storagePath, JSON.stringify(storage));
100
+ try {
101
+ if (storage) {
102
+ await writeFileSafe(storagePath, JSON.stringify(storage, null, 2));
103
+ }
104
+ } catch (error) {
105
+ console.error("error saving storage:", error);
106
+ }
71
107
  }
72
108
 
73
109
  function isGitRepository(path) {
@@ -75,12 +111,20 @@ function isGitRepository(path) {
75
111
  }
76
112
  async function getStagedDiff() {
77
113
  try {
78
- const fullDiff = (await x("git", ["diff", "--cached"])).stdout.toString();
79
- return fullDiff;
114
+ const diff = (await x("git", ["diff", "--cached"])).stdout.toString();
115
+ return diff;
80
116
  } catch {
81
117
  return null;
82
118
  }
83
119
  }
120
+ async function commit(message) {
121
+ try {
122
+ const result = await x("git", ["commit", "-m", message]);
123
+ return /file(s)? changed/i.test(result.stdout.toString());
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
84
128
 
85
129
  async function generateCommitMessage(diff) {
86
130
  const storage = await load();
@@ -106,56 +150,113 @@ ${diff}`
106
150
 
107
151
  yargs(hideBin(process.argv)).scriptName("noto").usage("$0 [args]").command(
108
152
  "config",
109
- "configure",
153
+ "setup you API key to enable noto.",
110
154
  () => {
111
155
  },
112
156
  async () => {
113
157
  const storage = await load();
114
158
  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
- });
159
+ const response2 = await prompts(
160
+ {
161
+ type: "confirm",
162
+ name: "reset",
163
+ message: "Do you want to reset your API key?"
164
+ },
165
+ {
166
+ onCancel: () => process.exit(0)
167
+ }
168
+ );
120
169
  if (!response2.reset) {
121
- console.log(`Use ${c.bold("`noto`")} to generate commit message!`);
170
+ console.log(
171
+ `Use ${c.greenBright(
172
+ c.bold("`noto`")
173
+ )} to generate your commit message!`
174
+ );
122
175
  return process.exit(0);
123
176
  }
124
177
  }
125
178
  const response = await prompts({
126
- type: "text",
179
+ type: "password",
127
180
  name: "apiKey",
128
- message: "Enter your API key:",
129
- validate: (value) => value ? true : "API key is required"
181
+ message: "Please enter your API key:",
182
+ validate: (value) => value ? true : "API key is required!"
130
183
  });
131
- storage.apiKey = response.apiKey;
132
- dump();
133
- console.log(`Use ${c.bold("`noto`")} to generate commit message!`);
184
+ if (response.apiKey) {
185
+ storage.apiKey = response.apiKey;
186
+ dump();
187
+ console.log("API key saved successfully!");
188
+ console.log(
189
+ `Use ${c.greenBright(
190
+ c.bold("`noto`")
191
+ )} to generate your commit message!`
192
+ );
193
+ }
134
194
  }
135
195
  ).command(
136
196
  "*",
137
197
  "generate commit message",
138
- () => {
198
+ (args) => {
199
+ args.option("copy", {
200
+ alias: "c",
201
+ type: "boolean",
202
+ description: "Copy the generated commit message to the clipboard."
203
+ });
204
+ args.option("apply", {
205
+ alias: "a",
206
+ type: "boolean",
207
+ description: "Commit the staged changes with the generated message."
208
+ });
139
209
  },
140
- async () => {
210
+ async (args) => {
141
211
  const storage = await load();
142
212
  if (!storage.apiKey) {
143
213
  console.log(
144
- `Please run ${c.bold("`noto config`")} to set your API key.`
214
+ `Please run ${c.cyan(c.bold("`noto config`"))} to set your API key.`
145
215
  );
146
216
  process.exit(1);
147
217
  }
148
218
  const cwd = process.cwd();
149
219
  if (!isGitRepository(cwd)) {
150
- console.log(c.red("hmm! git repository not found"));
220
+ console.log(
221
+ c.red("Oops! No Git repository found in the current directory.")
222
+ );
223
+ console.log(
224
+ `You can initialize one by running ${c.cyan(c.bold("`git init`"))}`
225
+ );
151
226
  process.exit(1);
152
227
  }
153
228
  const diff = await getStagedDiff();
154
229
  if (!diff) {
155
- console.log(c.red("hmm! no staged diff found"));
230
+ console.log(c.red("Oops! No staged changes found to commit."));
231
+ console.log(
232
+ `Stage changes with ${c.cyan(c.bold("`git add <file>`"))} or ${c.cyan(
233
+ c.bold("`git add .`")
234
+ )} for stage all files.`
235
+ );
236
+ process.exit(1);
237
+ }
238
+ const spin = spinner();
239
+ try {
240
+ spin.start("Generating commit message...");
241
+ const message = await generateCommitMessage(diff);
242
+ const copy = args.copy;
243
+ const apply = args.apply;
244
+ spin.success(`Commit Message: ${c.dim(c.bold(message))}`);
245
+ if (copy) {
246
+ clipboardy.writeSync(message);
247
+ spin.success("Message copied to clipboard!");
248
+ }
249
+ if (apply) {
250
+ spin.start("Committing staged changes...");
251
+ if (!await commit(message)) {
252
+ spin.fail("Failed to commit staged changes.");
253
+ process.exit(1);
254
+ }
255
+ spin.success("Staged changes committed successfully!");
256
+ }
257
+ } catch (error) {
258
+ spin.fail("Failed to generate commit message.");
156
259
  process.exit(1);
157
260
  }
158
- const message = await generateCommitMessage(diff);
159
- console.log(c.white(message));
160
261
  }
161
- ).alias("-v", "--version").alias("-h", "--help").argv;
262
+ ).version().alias("-v", "--version").alias("-h", "--help").argv;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@snelusha/noto",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.2.1",
5
5
  "description": "generate clean commit messages in a snap! ✨",
6
6
  "license": "MIT",
7
7
  "author": {
@@ -28,7 +28,8 @@
28
28
  ],
29
29
  "scripts": {
30
30
  "build": "unbuild",
31
- "start": "esno cli.ts"
31
+ "start": "esno cli.ts",
32
+ "publish": "npm publish --public"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/bun": "latest",
@@ -43,6 +44,8 @@
43
44
  "@ai-sdk/google": "^0.0.51",
44
45
  "@posva/prompts": "^2.4.4",
45
46
  "ai": "^3.4.9",
47
+ "clipboardy": "^4.0.0",
48
+ "ora": "^8.1.0",
46
49
  "picocolors": "^1.1.0",
47
50
  "tinyexec": "^0.3.0",
48
51
  "unbuild": "^2.0.0",