@snelusha/noto 1.2.8 → 1.3.0-beta.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 +2 -36
- package/dist/index.js +1071 -1025
- package/package.json +16 -11
- package/dist/index.d.ts +0 -2
package/dist/index.js
CHANGED
|
@@ -1,52 +1,95 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// src/utils/parser.ts
|
|
6
|
-
import arg from "arg";
|
|
7
|
-
var parse = (schema, raw) => {
|
|
8
|
-
const args = arg(schema, { argv: raw, permissive: true });
|
|
9
|
-
return {
|
|
10
|
-
command: args._[0],
|
|
11
|
-
options: args
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
var safeParse = (schema, raw) => {
|
|
15
|
-
let current2 = { ...schema };
|
|
16
|
-
let iterations = 0;
|
|
17
|
-
const maxIterations = Object.keys(current2).filter(
|
|
18
|
-
(key2) => current2[key2] === String
|
|
19
|
-
).length;
|
|
20
|
-
while (iterations++ < maxIterations) {
|
|
21
|
-
try {
|
|
22
|
-
return parse(current2, raw);
|
|
23
|
-
} catch (error) {
|
|
24
|
-
if (error.code === "ARG_MISSING_REQUIRED_LONGARG") {
|
|
25
|
-
const match = error.message.match(/(--\w[\w-]*)/);
|
|
26
|
-
if (match) {
|
|
27
|
-
const missingFlag = match[0];
|
|
28
|
-
if (current2[missingFlag] === String) {
|
|
29
|
-
current2[missingFlag] = Boolean;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
throw error;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return parse(current2, raw);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// src/commands/noto.ts
|
|
41
|
-
import * as p3 from "@clack/prompts";
|
|
42
|
-
import color3 from "picocolors";
|
|
43
|
-
import clipboard from "clipboardy";
|
|
2
|
+
import { createCli } from "trpc-cli";
|
|
3
|
+
// package.json
|
|
4
|
+
var version = "1.3.0-beta.0";
|
|
44
5
|
|
|
45
|
-
// src/
|
|
6
|
+
// src/trpc.ts
|
|
7
|
+
import fs3 from "node:fs/promises";
|
|
8
|
+
import { initTRPC } from "@trpc/server";
|
|
46
9
|
import * as p from "@clack/prompts";
|
|
47
10
|
import color from "picocolors";
|
|
48
11
|
import dedent from "dedent";
|
|
49
12
|
|
|
13
|
+
// src/utils/git.ts
|
|
14
|
+
import simpleGit from "simple-git";
|
|
15
|
+
var git = simpleGit();
|
|
16
|
+
var isGitRepository = async () => {
|
|
17
|
+
return git.checkIsRepo();
|
|
18
|
+
};
|
|
19
|
+
var getGitRoot = async () => {
|
|
20
|
+
try {
|
|
21
|
+
return await git.revparse(["--show-toplevel"]);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var getCommits = async (limit = 10) => {
|
|
27
|
+
try {
|
|
28
|
+
const log = await git.log({ maxCount: limit });
|
|
29
|
+
return log.all.map((c) => c.message);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var getStagedDiff = async () => {
|
|
35
|
+
try {
|
|
36
|
+
return git.diff(["--cached", "--", ":!*.lock"]);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var commit = async (message, amend) => {
|
|
42
|
+
try {
|
|
43
|
+
const options = amend ? { "--amend": null } : undefined;
|
|
44
|
+
const {
|
|
45
|
+
summary: { changes }
|
|
46
|
+
} = await git.commit(message, undefined, options);
|
|
47
|
+
return Boolean(changes);
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var push = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const result = await git.push();
|
|
55
|
+
return result.update || result.pushed && result.pushed.length > 0;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var getCurrentBranch = async () => {
|
|
61
|
+
try {
|
|
62
|
+
const branch = await git.branch();
|
|
63
|
+
return branch.current;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var getBranches = async (remote) => {
|
|
69
|
+
try {
|
|
70
|
+
const branches = await git.branch();
|
|
71
|
+
return remote ? branches.all : Object.keys(branches.branches).filter((b) => !b.startsWith("remotes/"));
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var checkout = async (branch) => {
|
|
77
|
+
try {
|
|
78
|
+
await git.checkout(branch, {});
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var checkoutLocalBranch = async (branch) => {
|
|
85
|
+
try {
|
|
86
|
+
await git.checkoutLocalBranch(branch);
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
50
93
|
// src/utils/storage.ts
|
|
51
94
|
import os from "os";
|
|
52
95
|
import { dirname, join, resolve } from "path";
|
|
@@ -75,13 +118,12 @@ var StorageSchema = z2.object({
|
|
|
75
118
|
apiKey: z2.string().optional(),
|
|
76
119
|
model: AvailableModelsSchema.optional().or(z2.string())
|
|
77
120
|
}).optional(),
|
|
78
|
-
lastGeneratedMessage: z2.string().optional()
|
|
121
|
+
lastGeneratedMessage: z2.string().optional(),
|
|
122
|
+
cache: z2.record(z2.string(), z2.string()).optional()
|
|
79
123
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"storage.sithi"
|
|
84
|
-
);
|
|
124
|
+
|
|
125
|
+
class StorageManager {
|
|
126
|
+
static storagePath = resolve(join(os.homedir(), ".config", "noto"), ".notorc");
|
|
85
127
|
static storage = {};
|
|
86
128
|
static async load() {
|
|
87
129
|
try {
|
|
@@ -89,8 +131,10 @@ var StorageManager = class {
|
|
|
89
131
|
const data = await fs.readFile(this.storagePath, "utf-8");
|
|
90
132
|
const json = data ? JSON.parse(data) : {};
|
|
91
133
|
const result = StorageSchema.safeParse(json);
|
|
92
|
-
if (!result.success)
|
|
93
|
-
|
|
134
|
+
if (!result.success)
|
|
135
|
+
this.storage = {};
|
|
136
|
+
else
|
|
137
|
+
this.storage = result.data;
|
|
94
138
|
} catch {
|
|
95
139
|
this.storage = {};
|
|
96
140
|
}
|
|
@@ -102,18 +146,17 @@ var StorageManager = class {
|
|
|
102
146
|
await fs.mkdir(directory, { recursive: true });
|
|
103
147
|
const data = JSON.stringify(this.storage, null, 2);
|
|
104
148
|
await fs.writeFile(this.storagePath, data, "utf-8");
|
|
105
|
-
} catch {
|
|
106
|
-
}
|
|
149
|
+
} catch {}
|
|
107
150
|
}
|
|
108
151
|
static async update(updater) {
|
|
109
152
|
try {
|
|
110
153
|
const updatedStorage = await updater(this.storage);
|
|
111
154
|
const result = StorageSchema.safeParse(updatedStorage);
|
|
112
|
-
if (!result.success)
|
|
155
|
+
if (!result.success)
|
|
156
|
+
return this.storage;
|
|
113
157
|
this.storage = result.data;
|
|
114
158
|
await this.save();
|
|
115
|
-
} catch {
|
|
116
|
-
}
|
|
159
|
+
} catch {}
|
|
117
160
|
return this.storage;
|
|
118
161
|
}
|
|
119
162
|
static async get() {
|
|
@@ -124,6 +167,38 @@ var StorageManager = class {
|
|
|
124
167
|
this.storage = {};
|
|
125
168
|
await this.save();
|
|
126
169
|
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/utils/fs.ts
|
|
173
|
+
import fs2 from "node:fs/promises";
|
|
174
|
+
import path from "node:path";
|
|
175
|
+
import { fileURLToPath } from "node:url";
|
|
176
|
+
var toPath = (urlOrPath) => urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;
|
|
177
|
+
async function findUp(name, options = {}) {
|
|
178
|
+
let directory = path.resolve(toPath(options.cwd ?? ""));
|
|
179
|
+
const { root } = path.parse(directory);
|
|
180
|
+
options.stopAt = path.resolve(toPath(options.stopAt ?? root));
|
|
181
|
+
const isAbsoluteName = path.isAbsolute(name);
|
|
182
|
+
while (directory) {
|
|
183
|
+
const filePath = isAbsoluteName ? name : path.join(directory, name);
|
|
184
|
+
try {
|
|
185
|
+
const stats = await fs2.stat(filePath);
|
|
186
|
+
if (options.type === undefined || options.type === "file" && stats.isFile() || options.type === "directory" && stats.isDirectory())
|
|
187
|
+
return filePath;
|
|
188
|
+
} catch {}
|
|
189
|
+
if (directory === options.stopAt || directory === root)
|
|
190
|
+
break;
|
|
191
|
+
directory = path.dirname(directory);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/utils/prompt.ts
|
|
196
|
+
var getPromptFile = async () => {
|
|
197
|
+
const root = await getGitRoot();
|
|
198
|
+
return await findUp(".noto/commit-prompt.md", {
|
|
199
|
+
stopAt: root || process.cwd(),
|
|
200
|
+
type: "file"
|
|
201
|
+
});
|
|
127
202
|
};
|
|
128
203
|
|
|
129
204
|
// src/utils/process.ts
|
|
@@ -133,147 +208,223 @@ var exit = async (code) => {
|
|
|
133
208
|
process.exit(code);
|
|
134
209
|
};
|
|
135
210
|
|
|
136
|
-
// src/
|
|
137
|
-
var
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
211
|
+
// src/trpc.ts
|
|
212
|
+
var t = initTRPC.meta().create({
|
|
213
|
+
defaultMeta: {
|
|
214
|
+
intro: true,
|
|
215
|
+
authRequired: true,
|
|
216
|
+
repoRequired: true,
|
|
217
|
+
diffRequired: false,
|
|
218
|
+
promptRequired: false
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
var authMiddleware = t.middleware(async (opts) => {
|
|
222
|
+
const { meta, next } = opts;
|
|
223
|
+
const storage = await StorageManager.get();
|
|
224
|
+
const apiKey = process.env.NOTO_API_KEY || storage.llm?.apiKey;
|
|
225
|
+
if (meta?.authRequired && !apiKey) {
|
|
226
|
+
p.log.error(dedent`${color.red("noto api key is missing.")}
|
|
227
|
+
${color.dim(`run ${color.cyan("`noto config key`")} to set it up.`)}`);
|
|
228
|
+
return await exit(1);
|
|
229
|
+
}
|
|
230
|
+
return next();
|
|
231
|
+
});
|
|
232
|
+
var gitMiddleware = t.middleware(async (opts) => {
|
|
233
|
+
const { meta, next } = opts;
|
|
234
|
+
const isRepository = await isGitRepository();
|
|
235
|
+
if (meta?.repoRequired && !isRepository) {
|
|
236
|
+
p.log.error(dedent`${color.red("no git repository found in cwd.")}
|
|
237
|
+
${color.dim(`run ${color.cyan("`git init`")} to initialize a new repository.`)}`);
|
|
238
|
+
return await exit(1);
|
|
239
|
+
}
|
|
240
|
+
const diff = isRepository && await getStagedDiff();
|
|
241
|
+
if (meta?.diffRequired && !diff) {
|
|
242
|
+
p.log.error(dedent`${color.red("no staged changes found.")}
|
|
243
|
+
${color.dim(`run ${color.cyan("`git add <file>`")} or ${color.cyan("`git add .`")} to stage changes.`)}`);
|
|
244
|
+
return await exit(1);
|
|
245
|
+
}
|
|
246
|
+
let prompt = null;
|
|
247
|
+
if (meta?.promptRequired) {
|
|
248
|
+
const promptPath = await getPromptFile();
|
|
249
|
+
if (promptPath) {
|
|
250
|
+
try {
|
|
251
|
+
prompt = await fs3.readFile(promptPath, "utf-8");
|
|
252
|
+
} catch {}
|
|
147
253
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
254
|
+
}
|
|
255
|
+
return next({
|
|
256
|
+
ctx: {
|
|
257
|
+
noto: {
|
|
258
|
+
prompt
|
|
259
|
+
},
|
|
260
|
+
git: {
|
|
261
|
+
isRepository,
|
|
262
|
+
diff
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
var baseProcedure = t.procedure.use((opts) => {
|
|
268
|
+
const { meta, next } = opts;
|
|
269
|
+
if (meta?.intro) {
|
|
270
|
+
console.log();
|
|
271
|
+
p.intro(`${color.bgCyan(color.black(" @snelusha/noto "))}`);
|
|
272
|
+
}
|
|
273
|
+
return next();
|
|
274
|
+
});
|
|
275
|
+
var authProcedure = baseProcedure.use(authMiddleware);
|
|
276
|
+
var gitProcedure = baseProcedure.use(gitMiddleware);
|
|
277
|
+
var authedGitProcedure = baseProcedure.use(authMiddleware).use(gitMiddleware);
|
|
151
278
|
|
|
152
|
-
// src/
|
|
279
|
+
// src/commands/checkout.ts
|
|
280
|
+
import { z as z3 } from "zod";
|
|
153
281
|
import * as p2 from "@clack/prompts";
|
|
154
282
|
import color2 from "picocolors";
|
|
155
|
-
import
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (regex.test(message)) return 0;
|
|
172
|
-
throw error;
|
|
283
|
+
import clipboard from "clipboardy";
|
|
284
|
+
var checkout2 = gitProcedure.meta({
|
|
285
|
+
description: "checkout a branch"
|
|
286
|
+
}).input(z3.object({
|
|
287
|
+
copy: z3.boolean().meta({
|
|
288
|
+
description: "copy the selected branch to clipboard",
|
|
289
|
+
alias: "c"
|
|
290
|
+
}),
|
|
291
|
+
create: z3.union([z3.boolean(), z3.string()]).optional().meta({ description: "create a new branch", alias: "b" }),
|
|
292
|
+
branch: z3.string().optional().meta({ positional: true })
|
|
293
|
+
})).mutation(async (opts) => {
|
|
294
|
+
const { input } = opts;
|
|
295
|
+
const branches = await getBranches();
|
|
296
|
+
if (!branches) {
|
|
297
|
+
p2.log.error("failed to fetch branches");
|
|
298
|
+
return await exit(1);
|
|
173
299
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
300
|
+
const currentBranch = await getCurrentBranch();
|
|
301
|
+
const targetBranch = typeof input.create === "string" ? input.create : input.branch;
|
|
302
|
+
const createFlag = input.create === true || typeof input.create === "string";
|
|
303
|
+
if (createFlag && targetBranch) {
|
|
304
|
+
if (branches.includes(targetBranch)) {
|
|
305
|
+
p2.log.error(`branch ${color2.red(targetBranch)} already exists in the repository`);
|
|
306
|
+
return await exit(1);
|
|
307
|
+
}
|
|
308
|
+
const result2 = await checkoutLocalBranch(targetBranch);
|
|
309
|
+
if (!result2) {
|
|
310
|
+
p2.log.error(`failed to create and checkout ${color2.bold(targetBranch)}`);
|
|
311
|
+
return await exit(1);
|
|
312
|
+
}
|
|
313
|
+
p2.log.success(`created and checked out ${color2.green(targetBranch)}`);
|
|
314
|
+
return await exit(0);
|
|
184
315
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
316
|
+
if (targetBranch) {
|
|
317
|
+
if (!branches.includes(targetBranch)) {
|
|
318
|
+
p2.log.error(`branch ${color2.red(targetBranch)} does not exist in the repository`);
|
|
319
|
+
const createBranch = await p2.confirm({
|
|
320
|
+
message: `do you want to create branch ${color2.green(targetBranch)}?`
|
|
321
|
+
});
|
|
322
|
+
if (p2.isCancel(createBranch)) {
|
|
323
|
+
p2.log.error("aborted");
|
|
324
|
+
return await exit(1);
|
|
325
|
+
}
|
|
326
|
+
if (createBranch) {
|
|
327
|
+
const result3 = await checkoutLocalBranch(targetBranch);
|
|
328
|
+
if (!result3) {
|
|
329
|
+
p2.log.error(`failed to create and checkout ${color2.bold(targetBranch)}`);
|
|
330
|
+
return await exit(1);
|
|
331
|
+
}
|
|
332
|
+
p2.log.success(`created and checked out ${color2.green(targetBranch)}`);
|
|
333
|
+
return await exit(0);
|
|
334
|
+
}
|
|
335
|
+
return await exit(1);
|
|
336
|
+
}
|
|
337
|
+
if (targetBranch === currentBranch) {
|
|
338
|
+
p2.log.error(`${color2.red("already on branch")} ${color2.green(targetBranch)}`);
|
|
339
|
+
return await exit(1);
|
|
340
|
+
}
|
|
341
|
+
const result2 = await checkout(targetBranch);
|
|
342
|
+
if (!result2) {
|
|
343
|
+
p2.log.error(`failed to checkout ${color2.bold(targetBranch)}`);
|
|
344
|
+
return await exit(1);
|
|
345
|
+
}
|
|
346
|
+
p2.log.success(`checked out ${color2.green(targetBranch)}`);
|
|
347
|
+
return await exit(0);
|
|
195
348
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const result = await git.push();
|
|
200
|
-
return result.update || result.pushed && result.pushed.length > 0;
|
|
201
|
-
} catch {
|
|
202
|
-
return false;
|
|
349
|
+
if (branches.length === 0) {
|
|
350
|
+
p2.log.error("no branches found in the repository");
|
|
351
|
+
return await exit(1);
|
|
203
352
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
353
|
+
const branch = await p2.select({
|
|
354
|
+
message: "select a branch to checkout",
|
|
355
|
+
options: branches.map((branch2) => ({
|
|
356
|
+
value: branch2,
|
|
357
|
+
label: color2.bold(branch2 === currentBranch ? color2.green(branch2) : branch2),
|
|
358
|
+
hint: branch2 === currentBranch ? "current branch" : undefined
|
|
359
|
+
})),
|
|
360
|
+
initialValue: currentBranch
|
|
361
|
+
});
|
|
362
|
+
if (p2.isCancel(branch)) {
|
|
363
|
+
p2.log.error("nothing selected!");
|
|
364
|
+
return await exit(1);
|
|
211
365
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const branches = await git.branch();
|
|
216
|
-
return remote ? branches.all : Object.keys(branches.branches).filter((b) => !b.startsWith("remotes/"));
|
|
217
|
-
} catch {
|
|
218
|
-
return null;
|
|
366
|
+
if (!branch) {
|
|
367
|
+
p2.log.error("no branch selected");
|
|
368
|
+
return await exit(1);
|
|
219
369
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
await
|
|
224
|
-
return true;
|
|
225
|
-
} catch {
|
|
226
|
-
return false;
|
|
370
|
+
if (input.copy) {
|
|
371
|
+
clipboard.writeSync(branch);
|
|
372
|
+
p2.log.success(`copied ${color2.green(branch)} to clipboard`);
|
|
373
|
+
return await exit(0);
|
|
227
374
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
await git.checkoutLocalBranch(branch);
|
|
232
|
-
return true;
|
|
233
|
-
} catch {
|
|
234
|
-
return false;
|
|
375
|
+
if (branch === currentBranch) {
|
|
376
|
+
p2.log.error(`${color2.red("already on branch")}`);
|
|
377
|
+
return await exit(1);
|
|
235
378
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return result.success;
|
|
241
|
-
} catch {
|
|
242
|
-
return false;
|
|
379
|
+
const result = await checkout(branch);
|
|
380
|
+
if (!result) {
|
|
381
|
+
p2.log.error(`failed to checkout ${color2.bold(branch)}`);
|
|
382
|
+
return await exit(1);
|
|
243
383
|
}
|
|
244
|
-
};
|
|
384
|
+
p2.log.success(`checked out ${color2.green(branch)}`);
|
|
385
|
+
await exit(0);
|
|
386
|
+
});
|
|
245
387
|
|
|
246
|
-
// src/
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
388
|
+
// src/commands/config/key.ts
|
|
389
|
+
import { z as z4 } from "zod";
|
|
390
|
+
import * as p3 from "@clack/prompts";
|
|
391
|
+
import color3 from "picocolors";
|
|
392
|
+
var key = baseProcedure.meta({ description: "configure noto api key" }).input(z4.string().optional().describe("apiKey")).mutation(async (opts) => {
|
|
393
|
+
const { input } = opts;
|
|
394
|
+
let apiKey = input;
|
|
395
|
+
if ((await StorageManager.get()).llm?.apiKey) {
|
|
396
|
+
const confirm3 = await p3.confirm({
|
|
397
|
+
message: "noto api key already configured, do you want to update it?"
|
|
398
|
+
});
|
|
399
|
+
if (p3.isCancel(confirm3) || !confirm3) {
|
|
400
|
+
p3.log.error(color3.red("nothing changed!"));
|
|
255
401
|
return await exit(1);
|
|
256
402
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return await exit(1);
|
|
266
|
-
}
|
|
267
|
-
opts.diff = diff;
|
|
403
|
+
}
|
|
404
|
+
if (!apiKey) {
|
|
405
|
+
const result = await p3.text({
|
|
406
|
+
message: "enter your noto api key"
|
|
407
|
+
});
|
|
408
|
+
if (p3.isCancel(result)) {
|
|
409
|
+
p3.log.error(color3.red("nothing changed!"));
|
|
410
|
+
return await exit(1);
|
|
268
411
|
}
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
412
|
+
apiKey = result;
|
|
413
|
+
}
|
|
414
|
+
await StorageManager.update((current) => ({
|
|
415
|
+
...current,
|
|
416
|
+
llm: {
|
|
417
|
+
...current.llm,
|
|
418
|
+
apiKey
|
|
419
|
+
}
|
|
420
|
+
}));
|
|
421
|
+
p3.log.success(color3.green("noto api key configured!"));
|
|
422
|
+
await exit(0);
|
|
423
|
+
});
|
|
272
424
|
|
|
273
|
-
// src/
|
|
274
|
-
import
|
|
275
|
-
import
|
|
276
|
-
import dedent3 from "dedent";
|
|
425
|
+
// src/commands/config/model.ts
|
|
426
|
+
import * as p4 from "@clack/prompts";
|
|
427
|
+
import color4 from "picocolors";
|
|
277
428
|
|
|
278
429
|
// src/ai/models.ts
|
|
279
430
|
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
@@ -290,905 +441,800 @@ var models = {
|
|
|
290
441
|
"gemini-1.5-pro-latest": google("gemini-1.5-pro-latest"),
|
|
291
442
|
"gemini-2.0-flash-001": google("gemini-2.0-flash-001"),
|
|
292
443
|
"gemini-2.0-flash": google("gemini-2.0-flash"),
|
|
293
|
-
"gemini-2.0-flash-lite-preview-02-05": google(
|
|
294
|
-
"gemini-2.0-flash-lite-preview-02-05"
|
|
295
|
-
),
|
|
444
|
+
"gemini-2.0-flash-lite-preview-02-05": google("gemini-2.0-flash-lite-preview-02-05"),
|
|
296
445
|
"gemini-2.5-flash-preview-04-17": google("gemini-2.5-flash-preview-04-17"),
|
|
297
446
|
"gemini-2.5-pro-preview-05-06": google("gemini-2.5-pro-preview-05-06")
|
|
298
447
|
};
|
|
299
448
|
var availableModels = Object.keys(models);
|
|
300
449
|
var getModel = async () => {
|
|
301
|
-
let
|
|
302
|
-
if (!
|
|
303
|
-
|
|
304
|
-
await StorageManager.update((
|
|
305
|
-
...
|
|
450
|
+
let model = (await StorageManager.get()).llm?.model;
|
|
451
|
+
if (!model || !availableModels.includes(model)) {
|
|
452
|
+
model = defaultModel;
|
|
453
|
+
await StorageManager.update((current) => ({
|
|
454
|
+
...current,
|
|
306
455
|
llm: {
|
|
307
|
-
...
|
|
456
|
+
...current.llm,
|
|
308
457
|
model: defaultModel
|
|
309
458
|
}
|
|
310
459
|
}));
|
|
311
460
|
}
|
|
312
|
-
return models[
|
|
461
|
+
return models[model];
|
|
313
462
|
};
|
|
314
463
|
|
|
315
|
-
// src/
|
|
316
|
-
var
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
1. **Commit Message Format:**
|
|
351
|
-
* The output MUST strictly follow the single-line format: \`<type>: <description>\`
|
|
352
|
-
* A single colon (\`:\`) followed by a single space MUST separate the \`<type>\` and \`<description>\`.
|
|
353
|
-
* Scopes within the type (e.g., \`feat(api):\`) are explicitly disallowed.
|
|
354
|
-
* Message bodies and footers are explicitly disallowed. The output MUST be exactly one line.
|
|
355
|
-
|
|
356
|
-
2. **Type (\`<type>\`) - Determination Logic:**
|
|
357
|
-
* **Priority 1: User-Specified Type:** Check the value provided on the \`USER_SPECIFIED_TYPE:\` line. If this value is exactly one of \`chore\`, \`feat\`, \`fix\`, \`docs\`, \`refactor\`, \`perf\`, \`test\`, then you **MUST** use this user-specified type.
|
|
358
|
-
* **Priority 2: Diff & Context Analysis (Default Behavior):** If the value on the \`USER_SPECIFIED_TYPE:\` line is \`[none]\` or invalid, you **MUST** select the \`<type>\` exclusively from the predefined vocabulary (\`chore\`, \`feat\`, \`fix\`, \`docs\`, \`refactor\`, \`perf\`, \`test\`). Base your selection on your analysis of the diff, **informed by the \`USER_PROVIDED_CONTEXT\`** if available, to accurately reflect the primary semantic purpose of the changes.
|
|
359
|
-
|
|
360
|
-
3. **Description (\`<description>\`):**
|
|
361
|
-
* **Tense:** MUST employ the imperative, present tense (e.g.,\`add\`, \`fix\`, \`update\`, \`implement\`, \`refactor\`, \`remove\`).
|
|
362
|
-
* **Content:** Must succinctly convey the core semantic change introduced by the diff.
|
|
363
|
-
* **Leverage Context:** If \`USER_PROVIDED_CONTEXT\` is available and not \`[none]\`, use it to **refine the description**, ensuring it reflects the *intent* and *purpose* behind the changes, while still accurately summarizing *what* was changed in the diff.
|
|
364
|
-
* **Focus:** Prioritize the most significant aspects of the change.
|
|
365
|
-
* **File Name Reference:** Inclusion of file names should be exceptional, reserved only for renaming operations or when essential for disambiguating the change's primary focus.
|
|
366
|
-
* **Case Sensitivity:** The entire output string, encompassing both \`<type>\` and \`<description>\`, MUST be rendered in lowercase.
|
|
367
|
-
* **Punctuation:** The description MUST NOT conclude with any terminal punctuation (e.g., no period/full stop).
|
|
368
|
-
* **Length Constraint:** The total character count of the generated commit message line MUST NOT exceed 72 characters. **The description must be concise enough to fit within this limit alongside the chosen type.**`
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
role: "user",
|
|
372
|
-
content: dedent3`
|
|
373
|
-
\`\`\`text
|
|
374
|
-
USER_SPECIFIED_TYPE: ${type ?? "[none]"}
|
|
375
|
-
|
|
376
|
-
USER_PROVIDED_CONTEXT:
|
|
377
|
-
${context ?? "[none]"}
|
|
378
|
-
\`\`\`
|
|
464
|
+
// src/commands/config/model.ts
|
|
465
|
+
var model = baseProcedure.meta({
|
|
466
|
+
description: "configure model"
|
|
467
|
+
}).mutation(async () => {
|
|
468
|
+
const model2 = await p4.select({
|
|
469
|
+
message: "select a model",
|
|
470
|
+
initialValue: (await StorageManager.get()).llm?.model,
|
|
471
|
+
options: Object.keys(models).map((model3) => ({
|
|
472
|
+
label: model3,
|
|
473
|
+
value: model3
|
|
474
|
+
}))
|
|
475
|
+
});
|
|
476
|
+
if (p4.isCancel(model2)) {
|
|
477
|
+
p4.log.error(color4.red("nothing changed!"));
|
|
478
|
+
return await exit(1);
|
|
479
|
+
}
|
|
480
|
+
if (model2 === "gemini-2.5-pro-preview-05-06") {
|
|
481
|
+
const confirm4 = await p4.confirm({
|
|
482
|
+
message: "this model does not have free quota tier, do you want to continue?"
|
|
483
|
+
});
|
|
484
|
+
if (p4.isCancel(confirm4) || !confirm4) {
|
|
485
|
+
p4.log.error(color4.red("nothing changed!"));
|
|
486
|
+
return await exit(1);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
await StorageManager.update((current) => ({
|
|
490
|
+
...current,
|
|
491
|
+
llm: {
|
|
492
|
+
...current.llm,
|
|
493
|
+
model: model2
|
|
494
|
+
}
|
|
495
|
+
}));
|
|
496
|
+
p4.log.success(color4.green("model configured!"));
|
|
497
|
+
await exit(0);
|
|
498
|
+
});
|
|
379
499
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
500
|
+
// src/commands/config/reset.ts
|
|
501
|
+
import * as p5 from "@clack/prompts";
|
|
502
|
+
import color5 from "picocolors";
|
|
503
|
+
var reset = baseProcedure.meta({
|
|
504
|
+
description: "reset the configuration"
|
|
505
|
+
}).mutation(async () => {
|
|
506
|
+
const confirm5 = await p5.confirm({
|
|
507
|
+
message: "are you sure you want to reset the configuration?"
|
|
385
508
|
});
|
|
386
|
-
|
|
387
|
-
|
|
509
|
+
if (p5.isCancel(confirm5) || !confirm5) {
|
|
510
|
+
p5.log.error(color5.red("nothing changed!"));
|
|
511
|
+
return await exit(1);
|
|
512
|
+
}
|
|
513
|
+
await StorageManager.clear();
|
|
514
|
+
p5.log.success(color5.green("configuration reset!"));
|
|
515
|
+
await exit(0);
|
|
516
|
+
});
|
|
388
517
|
|
|
389
|
-
// src/commands/
|
|
390
|
-
var
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
"refactor",
|
|
396
|
-
"perf",
|
|
397
|
-
"test"
|
|
398
|
-
];
|
|
399
|
-
var commitTypeOptions = availableTypes.map((type) => ({
|
|
400
|
-
label: type,
|
|
401
|
-
value: type
|
|
402
|
-
}));
|
|
403
|
-
var command = {
|
|
404
|
-
name: "noto",
|
|
405
|
-
description: "generate commit message",
|
|
406
|
-
usage: "noto [options]",
|
|
407
|
-
options: [
|
|
408
|
-
{
|
|
409
|
-
type: String,
|
|
410
|
-
flag: "--type",
|
|
411
|
-
alias: "-t",
|
|
412
|
-
description: "generate commit message based on type"
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
type: String,
|
|
416
|
-
flag: "--message",
|
|
417
|
-
alias: "-m",
|
|
418
|
-
description: "provide context for the commit message"
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
type: Boolean,
|
|
422
|
-
flag: "--copy",
|
|
423
|
-
alias: "-c",
|
|
424
|
-
description: "copy the generated commit message to clipboard"
|
|
425
|
-
},
|
|
426
|
-
{
|
|
427
|
-
type: Boolean,
|
|
428
|
-
flag: "--apply",
|
|
429
|
-
alias: "-a",
|
|
430
|
-
description: "commit the generated message directly"
|
|
431
|
-
},
|
|
432
|
-
{
|
|
433
|
-
type: Boolean,
|
|
434
|
-
flag: "--push",
|
|
435
|
-
alias: "-p",
|
|
436
|
-
description: "commit and push the changes"
|
|
437
|
-
},
|
|
438
|
-
{
|
|
439
|
-
type: Boolean,
|
|
440
|
-
flag: "--manual",
|
|
441
|
-
description: "commit and push the changes"
|
|
442
|
-
}
|
|
443
|
-
],
|
|
444
|
-
execute: withAuth(
|
|
445
|
-
withRepository(async (options) => {
|
|
446
|
-
const spin = p3.spinner();
|
|
447
|
-
try {
|
|
448
|
-
const { diff } = options;
|
|
449
|
-
const manual = options["--manual"];
|
|
450
|
-
if (manual) {
|
|
451
|
-
const message2 = await p3.text({
|
|
452
|
-
message: "edit the generated commit message",
|
|
453
|
-
placeholder: "chore: init repo"
|
|
454
|
-
});
|
|
455
|
-
if (p3.isCancel(message2)) {
|
|
456
|
-
p3.log.error(color3.red("nothing changed!"));
|
|
457
|
-
return await exit(1);
|
|
458
|
-
}
|
|
459
|
-
p3.log.step(color3.green(message2));
|
|
460
|
-
await StorageManager.update((current2) => ({
|
|
461
|
-
...current2,
|
|
462
|
-
lastGeneratedMessage: message2
|
|
463
|
-
}));
|
|
464
|
-
if (options["--apply"]) {
|
|
465
|
-
const success = await commit(message2);
|
|
466
|
-
if (success) {
|
|
467
|
-
p3.log.step(color3.dim("commit successful"));
|
|
468
|
-
} else {
|
|
469
|
-
p3.log.error(color3.red("failed to commit changes"));
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return await exit(0);
|
|
473
|
-
}
|
|
474
|
-
const type = options["--type"];
|
|
475
|
-
if (typeof type === "string" && !availableTypes.includes(type) || typeof type === "boolean") {
|
|
476
|
-
const type2 = await p3.select({
|
|
477
|
-
message: "select the type of commit message",
|
|
478
|
-
options: commitTypeOptions
|
|
479
|
-
});
|
|
480
|
-
if (p3.isCancel(type2)) {
|
|
481
|
-
p3.log.error(color3.red("nothing selected!"));
|
|
482
|
-
return await exit(1);
|
|
483
|
-
}
|
|
484
|
-
options.type = type2;
|
|
485
|
-
} else if (typeof type === "string") {
|
|
486
|
-
options.type = type;
|
|
487
|
-
}
|
|
488
|
-
const context = options["--message"];
|
|
489
|
-
if (typeof context === "string") {
|
|
490
|
-
options.context = context;
|
|
491
|
-
} else if (typeof context === "boolean") {
|
|
492
|
-
const context2 = await p3.text({
|
|
493
|
-
message: "provide context for the commit message",
|
|
494
|
-
placeholder: "describe the changes"
|
|
495
|
-
});
|
|
496
|
-
if (p3.isCancel(context2)) {
|
|
497
|
-
p3.log.error(color3.red("nothing changed!"));
|
|
498
|
-
return await exit(1);
|
|
499
|
-
}
|
|
500
|
-
options.context = context2;
|
|
501
|
-
}
|
|
502
|
-
spin.start("generating commit message");
|
|
503
|
-
let message = null;
|
|
504
|
-
if (!await isFirstCommit()) {
|
|
505
|
-
message = await generateCommitMessage(
|
|
506
|
-
diff,
|
|
507
|
-
options.type,
|
|
508
|
-
options.context
|
|
509
|
-
);
|
|
510
|
-
} else {
|
|
511
|
-
message = INIT_COMMIT_MESSAGE;
|
|
512
|
-
}
|
|
513
|
-
spin.stop(color3.white(message));
|
|
514
|
-
const editedMessage = await p3.text({
|
|
515
|
-
message: "edit the generated commit message",
|
|
516
|
-
initialValue: message,
|
|
517
|
-
placeholder: message
|
|
518
|
-
});
|
|
519
|
-
if (p3.isCancel(editedMessage)) {
|
|
520
|
-
p3.log.error(color3.red("nothing changed!"));
|
|
521
|
-
return await exit(1);
|
|
522
|
-
}
|
|
523
|
-
message = editedMessage;
|
|
524
|
-
p3.log.step(color3.green(message));
|
|
525
|
-
await StorageManager.update((current2) => ({
|
|
526
|
-
...current2,
|
|
527
|
-
lastGeneratedMessage: message
|
|
528
|
-
}));
|
|
529
|
-
if (options["--copy"]) {
|
|
530
|
-
clipboard.writeSync(message);
|
|
531
|
-
p3.log.step(color3.dim("copied commit message to clipboard"));
|
|
532
|
-
}
|
|
533
|
-
if (options["--apply"]) {
|
|
534
|
-
const success = await commit(message);
|
|
535
|
-
if (success) {
|
|
536
|
-
p3.log.step(color3.dim("commit successful"));
|
|
537
|
-
} else {
|
|
538
|
-
p3.log.error(color3.red("failed to commit changes"));
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
if (options["--push"]) {
|
|
542
|
-
const success = await push();
|
|
543
|
-
if (success) {
|
|
544
|
-
p3.log.step(color3.dim("push successful"));
|
|
545
|
-
} else {
|
|
546
|
-
p3.log.error(color3.red("failed to push changes"));
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
return await exit(0);
|
|
550
|
-
} catch {
|
|
551
|
-
spin.stop(color3.red("failed to generate commit message"), 1);
|
|
552
|
-
return await exit(1);
|
|
553
|
-
}
|
|
554
|
-
})
|
|
555
|
-
)
|
|
556
|
-
};
|
|
557
|
-
var noto_default = command;
|
|
518
|
+
// src/commands/config/index.ts
|
|
519
|
+
var config = t.router({
|
|
520
|
+
key,
|
|
521
|
+
model,
|
|
522
|
+
reset
|
|
523
|
+
});
|
|
558
524
|
|
|
559
525
|
// src/commands/prev.ts
|
|
560
|
-
import
|
|
561
|
-
import
|
|
562
|
-
import
|
|
526
|
+
import { z as z5 } from "zod";
|
|
527
|
+
import * as p6 from "@clack/prompts";
|
|
528
|
+
import color6 from "picocolors";
|
|
529
|
+
import dedent2 from "dedent";
|
|
563
530
|
import clipboard2 from "clipboardy";
|
|
564
|
-
var
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
531
|
+
var prev = gitProcedure.meta({
|
|
532
|
+
description: "access the last generated commit",
|
|
533
|
+
repoRequired: false
|
|
534
|
+
}).input(z5.object({
|
|
535
|
+
copy: z5.boolean().meta({ description: "copy the last commit to clipboard", alias: "c" }),
|
|
536
|
+
apply: z5.boolean().meta({ description: "commit the last generated message", alias: "a" }),
|
|
537
|
+
edit: z5.boolean().meta({
|
|
538
|
+
description: "edit the last generated commit message",
|
|
539
|
+
alias: "e"
|
|
540
|
+
}),
|
|
541
|
+
amend: z5.boolean().meta({
|
|
542
|
+
description: "amend the last commit with the last message"
|
|
543
|
+
})
|
|
544
|
+
})).mutation(async (opts) => {
|
|
545
|
+
const { input, ctx } = opts;
|
|
546
|
+
let lastGeneratedMessage = (await StorageManager.get()).lastGeneratedMessage;
|
|
547
|
+
if (!lastGeneratedMessage) {
|
|
548
|
+
p6.log.error(color6.red("no previous commit message found"));
|
|
549
|
+
return await exit(1);
|
|
550
|
+
}
|
|
551
|
+
const isEditMode = input.edit;
|
|
552
|
+
const isAmend = input.amend;
|
|
553
|
+
if (isAmend && !isEditMode) {
|
|
554
|
+
p6.log.error(color6.red("the --amend option requires the --edit option"));
|
|
555
|
+
return await exit(1);
|
|
556
|
+
}
|
|
557
|
+
p6.log.step(isEditMode ? color6.white(lastGeneratedMessage) : color6.green(lastGeneratedMessage));
|
|
558
|
+
if (isEditMode) {
|
|
559
|
+
const editedMessage = await p6.text({
|
|
560
|
+
message: "edit the last generated commit message",
|
|
561
|
+
initialValue: lastGeneratedMessage,
|
|
562
|
+
placeholder: lastGeneratedMessage
|
|
563
|
+
});
|
|
564
|
+
if (p6.isCancel(editedMessage)) {
|
|
565
|
+
p6.log.error(color6.red("nothing changed!"));
|
|
566
|
+
return await exit(1);
|
|
591
567
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
p4.log.step(
|
|
610
|
-
isEditMode ? color4.white(lastGeneratedMessage) : color4.green(lastGeneratedMessage)
|
|
611
|
-
);
|
|
612
|
-
if (options["--edit"]) {
|
|
613
|
-
const editedMessage = await p4.text({
|
|
614
|
-
message: "edit the last generated commit message",
|
|
615
|
-
initialValue: lastGeneratedMessage,
|
|
616
|
-
placeholder: lastGeneratedMessage
|
|
617
|
-
});
|
|
618
|
-
if (p4.isCancel(editedMessage)) {
|
|
619
|
-
p4.log.error(color4.red("nothing changed!"));
|
|
620
|
-
return await exit(1);
|
|
621
|
-
}
|
|
622
|
-
lastGeneratedMessage = editedMessage;
|
|
623
|
-
await StorageManager.update((current2) => ({
|
|
624
|
-
...current2,
|
|
625
|
-
lastGeneratedMessage: editedMessage
|
|
626
|
-
}));
|
|
627
|
-
p4.log.step(color4.green(lastGeneratedMessage));
|
|
628
|
-
}
|
|
629
|
-
if (options["--copy"]) {
|
|
630
|
-
clipboard2.writeSync(lastGeneratedMessage);
|
|
631
|
-
p4.log.step(
|
|
632
|
-
color4.dim("copied last generated commit message to clipboard")
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
if (options["--apply"] || isAmend) {
|
|
636
|
-
if (!options.isRepo) {
|
|
637
|
-
p4.log.error(
|
|
638
|
-
dedent4`${color4.red("no git repository found in cwd.")}
|
|
639
|
-
${color4.dim(`run ${color4.cyan("`git init`")} to initialize a new repository.`)}`
|
|
640
|
-
);
|
|
641
|
-
return await exit(1);
|
|
642
|
-
}
|
|
643
|
-
if (!options.diff && !isAmend) {
|
|
644
|
-
p4.log.error(
|
|
645
|
-
dedent4`${color4.red("no staged changes found.")}
|
|
646
|
-
${color4.dim(`run ${color4.cyan("`git add <file>`")} or ${color4.cyan("`git add .`")} to stage changes.`)}`
|
|
647
|
-
);
|
|
648
|
-
return await exit(1);
|
|
649
|
-
}
|
|
650
|
-
const success = await commit(lastGeneratedMessage, isAmend);
|
|
651
|
-
if (success) {
|
|
652
|
-
p4.log.step(color4.dim("commit successful"));
|
|
653
|
-
} else {
|
|
654
|
-
p4.log.error(color4.red("failed to commit changes"));
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return await exit(0);
|
|
658
|
-
},
|
|
659
|
-
{ enabled: false }
|
|
660
|
-
)
|
|
661
|
-
)
|
|
662
|
-
};
|
|
663
|
-
var prev_default = command2;
|
|
664
|
-
|
|
665
|
-
// src/commands/branch.ts
|
|
666
|
-
import * as p5 from "@clack/prompts";
|
|
667
|
-
import color5 from "picocolors";
|
|
668
|
-
import clipboard3 from "clipboardy";
|
|
669
|
-
import dedent5 from "dedent";
|
|
670
|
-
var current = {
|
|
671
|
-
name: "current",
|
|
672
|
-
description: "get current branch",
|
|
673
|
-
usage: "branch current",
|
|
674
|
-
options: [
|
|
675
|
-
{
|
|
676
|
-
type: Boolean,
|
|
677
|
-
flag: "--copy",
|
|
678
|
-
alias: "-c",
|
|
679
|
-
description: "copy the selected branch to clipboard"
|
|
568
|
+
lastGeneratedMessage = editedMessage;
|
|
569
|
+
await StorageManager.update((current) => ({
|
|
570
|
+
...current,
|
|
571
|
+
lastGeneratedMessage: editedMessage
|
|
572
|
+
}));
|
|
573
|
+
p6.log.step(color6.green(lastGeneratedMessage));
|
|
574
|
+
}
|
|
575
|
+
if (input.copy) {
|
|
576
|
+
clipboard2.writeSync(lastGeneratedMessage);
|
|
577
|
+
p6.log.step(color6.dim("copied last generated commit message to clipboard"));
|
|
578
|
+
}
|
|
579
|
+
if (input.apply || isAmend) {
|
|
580
|
+
if (!ctx.git.isRepository) {
|
|
581
|
+
p6.log.error(dedent2`${color6.red("no git repository found in cwd.")}
|
|
582
|
+
${color6.dim(`run ${color6.cyan("`git init`")} to initialize a new repository.`)}`);
|
|
583
|
+
return await exit(1);
|
|
680
584
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
p5.log.error(
|
|
686
|
-
dedent5`${color5.red("no git repository found in cwd.")}
|
|
687
|
-
${color5.dim(`run ${color5.cyan("`git init`")} to initialize a new repository.`)}`
|
|
688
|
-
);
|
|
689
|
-
return await exit(1);
|
|
690
|
-
}
|
|
691
|
-
const branch = await getCurrentBranch();
|
|
692
|
-
if (!branch) {
|
|
693
|
-
p5.log.error("failed to fetch current branch");
|
|
694
|
-
return await exit(1);
|
|
695
|
-
}
|
|
696
|
-
p5.log.success(`current branch: ${color5.bold(branch)}`);
|
|
697
|
-
if (options["--copy"]) {
|
|
698
|
-
clipboard3.writeSync(branch);
|
|
699
|
-
p5.log.success(`${color5.green("copied to clipboard!")}`);
|
|
700
|
-
}
|
|
701
|
-
await exit(0);
|
|
702
|
-
},
|
|
703
|
-
{ enabled: false }
|
|
704
|
-
)
|
|
705
|
-
};
|
|
706
|
-
var del = {
|
|
707
|
-
name: "delete",
|
|
708
|
-
description: "delete a branch",
|
|
709
|
-
usage: "branch delete <branch>",
|
|
710
|
-
options: [
|
|
711
|
-
{
|
|
712
|
-
type: Boolean,
|
|
713
|
-
flag: "--force",
|
|
714
|
-
alias: "-f",
|
|
715
|
-
description: "force delete a branch"
|
|
716
|
-
},
|
|
717
|
-
{
|
|
718
|
-
type: Boolean,
|
|
719
|
-
flag: "--all",
|
|
720
|
-
alias: "-a",
|
|
721
|
-
description: "select all branches except the current one"
|
|
585
|
+
if (!ctx.git.diff && !isAmend) {
|
|
586
|
+
p6.log.error(dedent2`${color6.red("no staged changes found.")}
|
|
587
|
+
${color6.dim(`run ${color6.cyan("`git add <file>`")} or ${color6.cyan("`git add .`")} to stage changes.`)}`);
|
|
588
|
+
return await exit(1);
|
|
722
589
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
dedent5`${color5.red("no git repository found in cwd.")}
|
|
729
|
-
${color5.dim(`run ${color5.cyan("`git init`")} to initialize a new repository.`)}`
|
|
730
|
-
);
|
|
731
|
-
return await exit(1);
|
|
732
|
-
}
|
|
733
|
-
const currentBranch = await getCurrentBranch();
|
|
734
|
-
const branches = await getBranches();
|
|
735
|
-
if (!currentBranch || !branches) {
|
|
736
|
-
p5.log.error("failed to fetch branches");
|
|
737
|
-
return await exit(1);
|
|
738
|
-
}
|
|
739
|
-
const selectedBranches = await p5.multiselect({
|
|
740
|
-
message: "select branches to delete",
|
|
741
|
-
initialValues: options["--all"] ? branches.filter((b) => b !== currentBranch) : [],
|
|
742
|
-
options: branches.map((branch) => ({
|
|
743
|
-
value: branch,
|
|
744
|
-
label: color5.bold(branch),
|
|
745
|
-
hint: branch === options["--current"] ? "current branch" : void 0
|
|
746
|
-
}))
|
|
747
|
-
});
|
|
748
|
-
if (p5.isCancel(selectedBranches)) {
|
|
749
|
-
p5.log.error("nothing selected!");
|
|
750
|
-
return await exit(1);
|
|
751
|
-
}
|
|
752
|
-
if (!selectedBranches) {
|
|
753
|
-
p5.log.error("no branch selected");
|
|
754
|
-
return await exit(1);
|
|
755
|
-
}
|
|
756
|
-
const force = options["--force"];
|
|
757
|
-
if (currentBranch && selectedBranches.includes(currentBranch)) {
|
|
758
|
-
p5.log.error("cannot delete current branch");
|
|
759
|
-
return await exit(1);
|
|
760
|
-
}
|
|
761
|
-
const deletedBranches = await deleteBranches(selectedBranches, force);
|
|
762
|
-
if (!deletedBranches) {
|
|
763
|
-
p5.log.error("failed to delete branches");
|
|
764
|
-
return await exit(1);
|
|
765
|
-
}
|
|
766
|
-
p5.log.success("branches deleted successfully");
|
|
767
|
-
await exit(0);
|
|
768
|
-
},
|
|
769
|
-
{ enabled: false }
|
|
770
|
-
)
|
|
771
|
-
};
|
|
772
|
-
var command3 = {
|
|
773
|
-
name: "branch",
|
|
774
|
-
description: "list branches",
|
|
775
|
-
usage: "branch [options]",
|
|
776
|
-
options: [
|
|
777
|
-
{
|
|
778
|
-
type: Boolean,
|
|
779
|
-
flag: "--remote",
|
|
780
|
-
alias: "-r",
|
|
781
|
-
description: "list branches including remotes"
|
|
782
|
-
},
|
|
783
|
-
{
|
|
784
|
-
type: Boolean,
|
|
785
|
-
flag: "--delete",
|
|
786
|
-
alias: "-d",
|
|
787
|
-
description: "delete a branch"
|
|
788
|
-
},
|
|
789
|
-
{
|
|
790
|
-
type: Boolean,
|
|
791
|
-
flag: "--force",
|
|
792
|
-
alias: "-f",
|
|
793
|
-
description: "force delete a branch"
|
|
794
|
-
},
|
|
795
|
-
{
|
|
796
|
-
type: Boolean,
|
|
797
|
-
flag: "--all",
|
|
798
|
-
alias: "-a",
|
|
799
|
-
description: "select all branches except the current one"
|
|
590
|
+
const success = await commit(lastGeneratedMessage, isAmend);
|
|
591
|
+
if (success) {
|
|
592
|
+
p6.log.step(color6.dim("commit successful"));
|
|
593
|
+
} else {
|
|
594
|
+
p6.log.error(color6.red("failed to commit changes"));
|
|
800
595
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if (!options.isRepo) {
|
|
805
|
-
p5.log.error(
|
|
806
|
-
dedent5`${color5.red("no git repository found in cwd.")}
|
|
807
|
-
${color5.dim(`run ${color5.cyan("`git init`")} to initialize a new repository.`)}`
|
|
808
|
-
);
|
|
809
|
-
return await exit(1);
|
|
810
|
-
}
|
|
811
|
-
if (options["--delete"]) return del.execute(options);
|
|
812
|
-
const remote = options["--remote"];
|
|
813
|
-
const branches = await getBranches(remote);
|
|
814
|
-
if (!branches) {
|
|
815
|
-
p5.log.error("failed to fetch branches");
|
|
816
|
-
return await exit(1);
|
|
817
|
-
}
|
|
818
|
-
const currentBranch = await getCurrentBranch();
|
|
819
|
-
const branch = await p5.select({
|
|
820
|
-
message: "select a branch",
|
|
821
|
-
options: branches.map((branch2) => ({
|
|
822
|
-
value: branch2,
|
|
823
|
-
label: color5.bold(
|
|
824
|
-
branch2 === currentBranch ? color5.green(branch2) : branch2
|
|
825
|
-
),
|
|
826
|
-
hint: branch2 === currentBranch ? "current branch" : void 0
|
|
827
|
-
})),
|
|
828
|
-
initialValue: currentBranch
|
|
829
|
-
});
|
|
830
|
-
if (p5.isCancel(branch)) {
|
|
831
|
-
p5.log.error("nothing selected!");
|
|
832
|
-
return await exit(1);
|
|
833
|
-
}
|
|
834
|
-
if (!branch) {
|
|
835
|
-
p5.log.error("no branch selected");
|
|
836
|
-
return await exit(1);
|
|
837
|
-
}
|
|
838
|
-
clipboard3.writeSync(branch);
|
|
839
|
-
p5.log.success(`${color5.green("copied to clipboard!")}`);
|
|
840
|
-
await exit(0);
|
|
841
|
-
},
|
|
842
|
-
{ enabled: false }
|
|
843
|
-
),
|
|
844
|
-
subCommands: [current, del]
|
|
845
|
-
};
|
|
846
|
-
var branch_default = command3;
|
|
596
|
+
}
|
|
597
|
+
return await exit(0);
|
|
598
|
+
});
|
|
847
599
|
|
|
848
|
-
// src/commands/
|
|
849
|
-
import
|
|
850
|
-
import
|
|
851
|
-
import
|
|
852
|
-
import
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
600
|
+
// src/commands/init.ts
|
|
601
|
+
import fs4 from "node:fs/promises";
|
|
602
|
+
import { z as z7 } from "zod";
|
|
603
|
+
import * as p7 from "@clack/prompts";
|
|
604
|
+
import color7 from "picocolors";
|
|
605
|
+
import dedent4 from "dedent";
|
|
606
|
+
|
|
607
|
+
// src/ai/index.ts
|
|
608
|
+
import { generateObject, wrapLanguageModel } from "ai";
|
|
609
|
+
import z6 from "zod";
|
|
610
|
+
import dedent3 from "dedent";
|
|
611
|
+
import superjson from "superjson";
|
|
612
|
+
|
|
613
|
+
// src/utils/hash.ts
|
|
614
|
+
import { createHash } from "crypto";
|
|
615
|
+
function hashString(content) {
|
|
616
|
+
const body = Buffer.from(content, "utf-8");
|
|
617
|
+
const header = Buffer.from(`blob ${body.length}\x00`, "utf-8");
|
|
618
|
+
return createHash("sha1").update(header).update(body).digest("hex");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/ai/index.ts
|
|
622
|
+
var COMMIT_GENERATOR_PROMPT = dedent3`
|
|
623
|
+
# System Instruction for Noto
|
|
624
|
+
|
|
625
|
+
You are a Git commit message generator for the \`noto\` CLI tool. Your role is to analyze staged code changes and generate clear, single-line commit messages that follow the user's established style.
|
|
626
|
+
|
|
627
|
+
## Your Purpose
|
|
628
|
+
|
|
629
|
+
Generate accurate, concise commit messages by analyzing git diffs and following user-provided style guidelines.
|
|
630
|
+
|
|
631
|
+
## Core Behavior
|
|
632
|
+
|
|
633
|
+
**Output Format:** Return ONLY a single-line commit message. No explanations, no markdown, no body, no footer.
|
|
634
|
+
|
|
635
|
+
**Response Style:**
|
|
636
|
+
- Be precise and factual based on the diff
|
|
637
|
+
- Follow the user's custom guidelines exactly
|
|
638
|
+
- Use clear, specific language
|
|
639
|
+
- Stay within 50-72 characters when possible
|
|
640
|
+
|
|
641
|
+
**Analysis Process:**
|
|
642
|
+
1. Read any user-provided context to understand the intent behind the changes
|
|
643
|
+
2. Examine the git diff to understand what changed technically
|
|
644
|
+
3. Combine user context and diff analysis to get complete understanding
|
|
645
|
+
4. Apply the user's style guidelines from \`.noto/commit-prompt.md\`
|
|
646
|
+
5. Classify the change type (feat, fix, refactor, docs, style, test, chore, etc.)
|
|
647
|
+
6. Generate a commit message that accurately describes both what changed and why
|
|
648
|
+
|
|
649
|
+
## Input Structure
|
|
650
|
+
|
|
651
|
+
You will receive:
|
|
652
|
+
\`\`\`
|
|
653
|
+
USER GUIDELINES:
|
|
654
|
+
[Custom style from .noto/commit-prompt.md]
|
|
655
|
+
|
|
656
|
+
USER CONTEXT (optional):
|
|
657
|
+
[Additional context or description provided by the user about the changes]
|
|
658
|
+
|
|
659
|
+
GIT DIFF:
|
|
660
|
+
[Staged changes]
|
|
661
|
+
\`\`\`
|
|
662
|
+
|
|
663
|
+
If user context is provided, use it to better understand the intent and purpose of the changes. Combine this context with your diff analysis to generate a more accurate and meaningful commit message.
|
|
664
|
+
|
|
665
|
+
## Output Requirements
|
|
666
|
+
|
|
667
|
+
Return only the commit message text:
|
|
668
|
+
\`\`\`
|
|
669
|
+
type(scope): description
|
|
670
|
+
\`\`\`
|
|
671
|
+
|
|
672
|
+
Or whatever format matches the user's guidelines. **Must be single-line only.**
|
|
673
|
+
|
|
674
|
+
## Quality Standards
|
|
675
|
+
|
|
676
|
+
- Accurately reflect the changes in the diff
|
|
677
|
+
- Incorporate user-provided context when available to capture the "why"
|
|
678
|
+
- Follow user's format, tense, and capitalization preferences
|
|
679
|
+
- Be specific (avoid vague terms like "update" or "change")
|
|
680
|
+
- Use proper grammar
|
|
681
|
+
- Make the git history useful and searchable
|
|
682
|
+
- Balance technical accuracy with user intent
|
|
683
|
+
|
|
684
|
+
## What NOT to Do
|
|
685
|
+
|
|
686
|
+
- Don't add explanations or commentary
|
|
687
|
+
- Don't generate multi-line messages
|
|
688
|
+
- Don't make assumptions beyond the visible diff
|
|
689
|
+
- Don't ignore the user's style guidelines
|
|
690
|
+
- Don't use generic or vague descriptions
|
|
691
|
+
|
|
692
|
+
**Remember:** Your output becomes permanent git history. Generate commit messages that are clear, accurate, and consistent with the user's established patterns.
|
|
693
|
+
`;
|
|
694
|
+
var DEFAULT_COMMIT_GUIDELINES = dedent3`
|
|
695
|
+
# Commit Message Guidelines
|
|
696
|
+
|
|
697
|
+
## Format
|
|
698
|
+
Use conventional commits: \`type(scope): description\`
|
|
699
|
+
|
|
700
|
+
The scope is optional but recommended when changes affect a specific component or area.
|
|
701
|
+
|
|
702
|
+
## Style Rules
|
|
703
|
+
- **Tense**: Imperative present tense (e.g., "add" not "added" or "adds")
|
|
704
|
+
- **Capitalization**: Lowercase for the first letter of description
|
|
705
|
+
- **Length**: Keep the entire message under 72 characters, ideally around 50
|
|
706
|
+
- **Tone**: Clear, concise, and professional
|
|
707
|
+
|
|
708
|
+
## Commit Types
|
|
709
|
+
- \`feat\`: New feature or functionality for the user
|
|
710
|
+
- \`fix\`: Bug fix that resolves an issue
|
|
711
|
+
- \`docs\`: Documentation changes only
|
|
712
|
+
- \`style\`: Code style changes (formatting, missing semicolons, etc.) with no logic changes
|
|
713
|
+
- \`refactor\`: Code changes that neither fix bugs nor add features
|
|
714
|
+
- \`perf\`: Performance improvements
|
|
715
|
+
- \`test\`: Adding or updating tests
|
|
716
|
+
- \`build\`: Changes to build system or dependencies (npm, webpack, etc.)
|
|
717
|
+
- \`ci\`: Changes to CI/CD configuration files and scripts
|
|
718
|
+
- \`chore\`: Routine tasks, maintenance, or tooling changes
|
|
719
|
+
- \`revert\`: Revert a previous commit
|
|
720
|
+
|
|
721
|
+
## Scope Usage
|
|
722
|
+
**Prefer omitting scopes whenever possible.** Only include a scope when it significantly clarifies which part of the codebase is affected.
|
|
723
|
+
|
|
724
|
+
Use scopes sparingly and only when the change is isolated to a specific area:
|
|
725
|
+
- Component or module names (e.g., \`auth\`, \`api\`, \`ui\`, \`parser\`)
|
|
726
|
+
- Feature areas (e.g., \`login\`, \`checkout\`, \`dashboard\`)
|
|
727
|
+
- File or directory names when appropriate
|
|
728
|
+
|
|
729
|
+
Omit scope for:
|
|
730
|
+
- Changes that affect the entire project
|
|
731
|
+
- Changes that don't fit a specific area
|
|
732
|
+
- Most commits where the type and description are clear enough
|
|
733
|
+
- When in doubt, leave it out
|
|
734
|
+
|
|
735
|
+
## Description Patterns
|
|
736
|
+
- Start with a verb in imperative mood (add, update, remove, fix, implement, etc.)
|
|
737
|
+
- Be specific about what changed, not how it changed
|
|
738
|
+
- Focus on the "what" and "why", not the "how"
|
|
739
|
+
- Avoid ending with a period
|
|
740
|
+
- Keep it clear enough that someone can understand the change without reading the code
|
|
741
|
+
|
|
742
|
+
## Examples
|
|
743
|
+
- \`feat: add OAuth2 authentication support\`
|
|
744
|
+
- \`fix: resolve timeout issue in user endpoint\`
|
|
745
|
+
- \`docs: update installation instructions in README\`
|
|
746
|
+
- \`style: fix indentation and spacing\`
|
|
747
|
+
- \`refactor: simplify data parsing logic\`
|
|
748
|
+
- \`perf: optimize database query performance\`
|
|
749
|
+
- \`test: add unit tests for login validation\`
|
|
750
|
+
- \`build: upgrade webpack to version 5\`
|
|
751
|
+
- \`ci: add automated deployment workflow\`
|
|
752
|
+
- \`chore: update dependencies to latest versions\`
|
|
753
|
+
|
|
754
|
+
Examples with scope (use only when necessary):
|
|
755
|
+
- \`feat(auth): add biometric authentication\`
|
|
756
|
+
- \`fix(api): handle null response in user service\`
|
|
757
|
+
|
|
758
|
+
## Breaking Changes
|
|
759
|
+
For breaking changes, add \`!\` after the type/scope:
|
|
760
|
+
- \`feat!: remove deprecated API endpoints\`
|
|
761
|
+
- \`refactor(api)!: change response format to JSON:API spec\`
|
|
762
|
+
|
|
763
|
+
## Additional Notes
|
|
764
|
+
- If a commit addresses a specific issue, you can reference it in the description (e.g., \`fix: resolve memory leak (fixes #123)\`)
|
|
765
|
+
- Each commit should represent a single logical change
|
|
766
|
+
- Write commits as if completing the sentence: "If applied, this commit will..."`;
|
|
767
|
+
var GUIDELINES_GENERATOR_PROMPT = dedent3`
|
|
768
|
+
You are a commit style analyzer. Analyze the provided commit history and generate a personalized style guide that will be used to generate future commit messages.
|
|
769
|
+
|
|
770
|
+
## Task
|
|
771
|
+
|
|
772
|
+
Analyze the commit messages below and create clear guidelines that capture the user's commit message style and patterns.
|
|
773
|
+
|
|
774
|
+
## Input Format
|
|
775
|
+
|
|
776
|
+
You will receive a list of commit messages from the user's git history:
|
|
777
|
+
|
|
778
|
+
\`\`\`
|
|
779
|
+
COMMIT HISTORY:
|
|
780
|
+
[List of previous commit messages]
|
|
781
|
+
\`\`\`
|
|
782
|
+
|
|
783
|
+
## Output Format
|
|
784
|
+
|
|
785
|
+
Generate a markdown document with clear, actionable guidelines. Use this structure:
|
|
786
|
+
|
|
787
|
+
\`\`\`markdown
|
|
788
|
+
# Commit Message Guidelines
|
|
789
|
+
|
|
790
|
+
## Format
|
|
791
|
+
[Describe the exact format: conventional commits, custom format, etc.]
|
|
792
|
+
|
|
793
|
+
## Style Rules
|
|
794
|
+
- **Tense**: [present/past/imperative]
|
|
795
|
+
- **Capitalization**: [first letter uppercase/lowercase/varies]
|
|
796
|
+
- **Length**: [typical character count or "concise"/"detailed"]
|
|
797
|
+
- **Tone**: [technical/casual/formal]
|
|
798
|
+
|
|
799
|
+
## Commit Types
|
|
800
|
+
[List the types they use with brief descriptions]
|
|
801
|
+
- \`type\`: When to use this type
|
|
802
|
+
|
|
803
|
+
## Scope Usage
|
|
804
|
+
[If they use scopes, describe the pattern. If not, say "No scopes used"]
|
|
805
|
+
|
|
806
|
+
## Description Patterns
|
|
807
|
+
[How they write descriptions - specific patterns, keywords, style]
|
|
808
|
+
|
|
809
|
+
## Examples from History
|
|
810
|
+
[Include 3-5 actual examples from their commits]
|
|
811
|
+
\`\`\`
|
|
812
|
+
|
|
813
|
+
## Analysis Guidelines
|
|
814
|
+
|
|
815
|
+
**IMPORTANT**: Ignore merge commits when analyzing style. Skip any commits that:
|
|
816
|
+
- Start with "Merge pull request"
|
|
817
|
+
- Start with "Merge branch"
|
|
818
|
+
- Start with "Merge remote-tracking branch"
|
|
819
|
+
- Contain "Merge" as the primary action
|
|
820
|
+
|
|
821
|
+
Focus only on regular commits (features, fixes, refactors, etc.) for style analysis.
|
|
822
|
+
|
|
823
|
+
When analyzing commits, look for:
|
|
824
|
+
|
|
825
|
+
1. **Structure Patterns**:
|
|
826
|
+
- Do they follow conventional commits? (type: description or type(scope): description)
|
|
827
|
+
- Is there a consistent format?
|
|
828
|
+
- Any special characters or prefixes?
|
|
829
|
+
|
|
830
|
+
2. **Commit Types**:
|
|
831
|
+
- What types do they use? (feat, fix, docs, refactor, style, test, chore, etc.)
|
|
832
|
+
- Are types consistent or mixed?
|
|
833
|
+
- Any custom types?
|
|
834
|
+
|
|
835
|
+
3. **Scope Patterns**:
|
|
836
|
+
- Do they use scopes in parentheses?
|
|
837
|
+
- What scopes appear frequently?
|
|
838
|
+
- Are scopes specific (file/component names) or general (area names)?
|
|
839
|
+
|
|
840
|
+
4. **Writing Style**:
|
|
841
|
+
- Present tense ("add feature") vs past tense ("added feature") vs imperative ("add feature")
|
|
842
|
+
- First letter capitalized or lowercase?
|
|
843
|
+
- Typical length - short and concise or longer and detailed?
|
|
844
|
+
- Technical terminology or simple language?
|
|
845
|
+
|
|
846
|
+
5. **Common Patterns**:
|
|
847
|
+
- Repeated keywords or phrases
|
|
848
|
+
- How they describe features vs fixes vs refactors
|
|
849
|
+
- Any emoji usage?
|
|
850
|
+
- Any ticket/issue references?
|
|
851
|
+
|
|
852
|
+
## Quality Requirements
|
|
853
|
+
|
|
854
|
+
The generated guidelines must be:
|
|
855
|
+
- ✓ **Clear and specific** - no vague statements
|
|
856
|
+
- ✓ **Actionable** - easy to follow when generating new commits
|
|
857
|
+
- ✓ **Accurate** - truly reflect the user's style
|
|
858
|
+
- ✓ **Concise** - keep it under 300 words
|
|
859
|
+
- ✓ **Consistent** - don't contradict yourself
|
|
860
|
+
|
|
861
|
+
## Special Cases
|
|
862
|
+
|
|
863
|
+
**If commits are inconsistent**: Choose the most frequent pattern and note: "Style varies, but most commonly uses [pattern]"
|
|
864
|
+
|
|
865
|
+
**If very few commits after filtering merges**: Note: "Limited commit history available. Using conventional commits as base with observed patterns."
|
|
866
|
+
|
|
867
|
+
**If only merge commits exist**: Generate standard conventional commits guidelines and note: "No regular commits found in history. Using conventional commits format."
|
|
868
|
+
|
|
869
|
+
**If commits are very simple**: That's fine! Note: "Prefers simple, straightforward commit messages"
|
|
870
|
+
|
|
871
|
+
**If no clear pattern**: Generate sensible conventional commit guidelines with a note: "No strong pattern detected. Recommending conventional commits format."
|
|
872
|
+
|
|
873
|
+
## Example Analysis
|
|
874
|
+
|
|
875
|
+
Given these commits:
|
|
876
|
+
\`\`\`
|
|
877
|
+
feat(auth): add OAuth2 login support
|
|
878
|
+
fix(api): resolve timeout issue in user endpoint
|
|
879
|
+
refactor: simplify database connection logic
|
|
880
|
+
docs: update README with setup instructions
|
|
881
|
+
feat(ui): implement dark mode toggle
|
|
882
|
+
\`\`\`
|
|
883
|
+
|
|
884
|
+
Generate guidelines like:
|
|
885
|
+
\`\`\`markdown
|
|
886
|
+
# Commit Message Guidelines
|
|
887
|
+
|
|
888
|
+
## Format
|
|
889
|
+
Use conventional commits: \`type(scope): description\`
|
|
890
|
+
|
|
891
|
+
## Style Rules
|
|
892
|
+
- **Tense**: Imperative/present ("add", "fix", "implement")
|
|
893
|
+
- **Capitalization**: Lowercase first letter
|
|
894
|
+
- **Length**: Concise, 40-60 characters
|
|
895
|
+
- **Tone**: Technical and specific
|
|
896
|
+
|
|
897
|
+
## Commit Types
|
|
898
|
+
- \`feat\`: New features or capabilities
|
|
899
|
+
- \`fix\`: Bug fixes and issue resolutions
|
|
900
|
+
- \`refactor\`: Code improvements without new features
|
|
901
|
+
- \`docs\`: Documentation updates
|
|
902
|
+
|
|
903
|
+
## Scope Usage
|
|
904
|
+
Use specific component/area names in parentheses (auth, api, ui). Omit scope for general changes like refactoring.
|
|
905
|
+
|
|
906
|
+
## Description Patterns
|
|
907
|
+
Start with action verb (add, implement, resolve, update, simplify). Be specific about what changed. Reference the component or feature affected.
|
|
908
|
+
|
|
909
|
+
## Examples from History
|
|
910
|
+
- feat(auth): add OAuth2 login support
|
|
911
|
+
- fix(api): resolve timeout issue in user endpoint
|
|
912
|
+
- refactor: simplify database connection logic
|
|
913
|
+
\`\`\`
|
|
914
|
+
|
|
915
|
+
## Important Notes
|
|
916
|
+
|
|
917
|
+
- Focus on patterns, not on individual commit content
|
|
918
|
+
- Generate guidelines that will work for future commits, not just explain past ones
|
|
919
|
+
- Keep it practical and easy to follow
|
|
920
|
+
- The output will be stored as \`.noto/commit-prompt.md\` and used by an AI to generate commits
|
|
921
|
+
|
|
922
|
+
Generate the markdown guidelines now based on the commit history provided.
|
|
923
|
+
`;
|
|
924
|
+
var cacheMiddleware = {
|
|
925
|
+
wrapGenerate: async ({ doGenerate, params }) => {
|
|
926
|
+
const key2 = hashString(JSON.stringify(params));
|
|
927
|
+
const cache = (await StorageManager.get()).cache;
|
|
928
|
+
if (cache && key2 in cache) {
|
|
929
|
+
const cached = cache[key2];
|
|
930
|
+
return superjson.parse(cached);
|
|
869
931
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
dedent6`${color6.red("no git repository found in cwd.")}
|
|
877
|
-
${color6.dim(`run ${color6.cyan("`git init`")} to initialize a new repository.`)}`
|
|
878
|
-
);
|
|
879
|
-
return await exit(1);
|
|
880
|
-
}
|
|
881
|
-
const branches = await getBranches();
|
|
882
|
-
if (!branches) {
|
|
883
|
-
p6.log.error("failed to fetch branches");
|
|
884
|
-
return await exit(1);
|
|
885
|
-
}
|
|
886
|
-
const currentBranch = await getCurrentBranch();
|
|
887
|
-
const branchName = args[0];
|
|
888
|
-
if (options["--create"]) {
|
|
889
|
-
if (branches.includes(branchName)) {
|
|
890
|
-
p6.log.error(
|
|
891
|
-
`branch ${color6.red(branchName)} already exists in the repository`
|
|
892
|
-
);
|
|
893
|
-
return await exit(1);
|
|
894
|
-
}
|
|
895
|
-
const result2 = await checkoutLocalBranch(branchName);
|
|
896
|
-
if (!result2) {
|
|
897
|
-
p6.log.error(
|
|
898
|
-
`failed to create and checkout ${color6.bold(branchName)}`
|
|
899
|
-
);
|
|
900
|
-
return await exit(1);
|
|
901
|
-
}
|
|
902
|
-
p6.log.success(`created and checked out ${color6.green(branchName)}`);
|
|
903
|
-
return await exit(0);
|
|
904
|
-
}
|
|
905
|
-
if (branchName) {
|
|
906
|
-
if (!branches.includes(branchName)) {
|
|
907
|
-
p6.log.error(
|
|
908
|
-
`branch ${color6.red(branchName)} does not exist in the repository`
|
|
909
|
-
);
|
|
910
|
-
const createBranch = await p6.confirm({
|
|
911
|
-
message: `do you want to create branch ${color6.green(branchName)}?`
|
|
912
|
-
});
|
|
913
|
-
if (p6.isCancel(createBranch)) {
|
|
914
|
-
p6.log.error("aborted");
|
|
915
|
-
return await exit(1);
|
|
916
|
-
}
|
|
917
|
-
if (createBranch) {
|
|
918
|
-
const result3 = await checkoutLocalBranch(branchName);
|
|
919
|
-
if (!result3) {
|
|
920
|
-
p6.log.error(
|
|
921
|
-
`failed to create and checkout ${color6.bold(branchName)}`
|
|
922
|
-
);
|
|
923
|
-
return await exit(1);
|
|
924
|
-
}
|
|
925
|
-
p6.log.success(`created and checked out ${color6.green(branchName)}`);
|
|
926
|
-
return await exit(0);
|
|
927
|
-
}
|
|
928
|
-
return await exit(1);
|
|
929
|
-
}
|
|
930
|
-
if (branchName === currentBranch) {
|
|
931
|
-
p6.log.error(
|
|
932
|
-
`${color6.red("already on branch")} ${color6.green(branchName)}`
|
|
933
|
-
);
|
|
934
|
-
return await exit(1);
|
|
935
|
-
}
|
|
936
|
-
const result2 = await checkout(branchName);
|
|
937
|
-
if (!result2) {
|
|
938
|
-
p6.log.error(`failed to checkout ${color6.bold(branchName)}`);
|
|
939
|
-
return await exit(1);
|
|
932
|
+
const result = await doGenerate();
|
|
933
|
+
await StorageManager.update((current) => {
|
|
934
|
+
return {
|
|
935
|
+
...current,
|
|
936
|
+
cache: {
|
|
937
|
+
[key2]: superjson.stringify(result)
|
|
940
938
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
939
|
+
};
|
|
940
|
+
});
|
|
941
|
+
return result;
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
var generateCommitMessage = async (diff, prompt, context, forceCache = false) => {
|
|
945
|
+
const model2 = await getModel();
|
|
946
|
+
const { object } = await generateObject({
|
|
947
|
+
model: !forceCache ? wrapLanguageModel({
|
|
948
|
+
model: model2,
|
|
949
|
+
middleware: cacheMiddleware
|
|
950
|
+
}) : model2,
|
|
951
|
+
schema: z6.object({
|
|
952
|
+
message: z6.string()
|
|
953
|
+
}),
|
|
954
|
+
messages: [
|
|
955
|
+
{
|
|
956
|
+
role: "system",
|
|
957
|
+
content: COMMIT_GENERATOR_PROMPT
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
role: "user",
|
|
961
|
+
content: dedent3`
|
|
962
|
+
USER GUIDELINES:
|
|
963
|
+
${prompt ?? DEFAULT_COMMIT_GUIDELINES}
|
|
964
|
+
${context ? `
|
|
965
|
+
USER CONTEXT:
|
|
966
|
+
${context}` : ""}
|
|
967
|
+
|
|
968
|
+
GIT DIFF:
|
|
969
|
+
${diff}
|
|
970
|
+
`
|
|
971
971
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
972
|
+
]
|
|
973
|
+
});
|
|
974
|
+
return object.message.trim();
|
|
975
|
+
};
|
|
976
|
+
var generateCommitGuidelines = async (commits) => {
|
|
977
|
+
const model2 = await getModel();
|
|
978
|
+
const { object } = await generateObject({
|
|
979
|
+
model: model2,
|
|
980
|
+
schema: z6.object({
|
|
981
|
+
prompt: z6.string()
|
|
982
|
+
}),
|
|
983
|
+
messages: [
|
|
984
|
+
{
|
|
985
|
+
role: "system",
|
|
986
|
+
content: GUIDELINES_GENERATOR_PROMPT
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
role: "user",
|
|
990
|
+
content: dedent3`
|
|
991
|
+
COMMIT HISTORY:
|
|
992
|
+
${commits.join(`
|
|
993
|
+
`)}`
|
|
976
994
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
{ enabled: false }
|
|
981
|
-
)
|
|
995
|
+
]
|
|
996
|
+
});
|
|
997
|
+
return object.prompt.trim();
|
|
982
998
|
};
|
|
983
|
-
var checkout_default = command4;
|
|
984
999
|
|
|
985
|
-
// src/commands/
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1000
|
+
// src/commands/init.ts
|
|
1001
|
+
var EMPTY_TEMPLATE = dedent4`
|
|
1002
|
+
# Commit Message Guidelines
|
|
1003
|
+
|
|
1004
|
+
# Add your custom guidelines here.
|
|
1005
|
+
# When no guidelines are present, noto will use conventional commits format by default.`;
|
|
1006
|
+
var init = gitProcedure.meta({
|
|
1007
|
+
description: "initialize noto in the repository"
|
|
1008
|
+
}).input(z7.object({
|
|
1009
|
+
root: z7.boolean().meta({
|
|
1010
|
+
description: "create the prompt file in the git root"
|
|
1011
|
+
}),
|
|
1012
|
+
generate: z7.boolean().meta({
|
|
1013
|
+
description: "generate a prompt file based on existing commits"
|
|
1014
|
+
})
|
|
1015
|
+
})).mutation(async (opts) => {
|
|
1016
|
+
const { input } = opts;
|
|
1017
|
+
const root = await getGitRoot();
|
|
1018
|
+
let promptFile = root;
|
|
1019
|
+
const cwd = process.cwd();
|
|
1020
|
+
const existingPromptFile = await getPromptFile();
|
|
1021
|
+
let prompt = null;
|
|
1022
|
+
if (existingPromptFile) {
|
|
1023
|
+
if (!existingPromptFile.startsWith(cwd)) {
|
|
1024
|
+
p7.log.warn(dedent4`${color7.yellow("a prompt file already exists!")}
|
|
1025
|
+
${color7.gray(existingPromptFile)}`);
|
|
1026
|
+
const shouldContinue = await p7.confirm({
|
|
1027
|
+
message: "do you want to create in the current directory instead?",
|
|
1028
|
+
initialValue: true
|
|
996
1029
|
});
|
|
997
|
-
if (p7.isCancel(
|
|
998
|
-
p7.log.error(
|
|
1030
|
+
if (p7.isCancel(shouldContinue) || !shouldContinue) {
|
|
1031
|
+
p7.log.error("aborted");
|
|
999
1032
|
return await exit(1);
|
|
1000
1033
|
}
|
|
1034
|
+
promptFile = cwd;
|
|
1035
|
+
} else {
|
|
1036
|
+
p7.log.error(dedent4`${color7.red("a prompt file already exists.")}
|
|
1037
|
+
${color7.gray(existingPromptFile)}`);
|
|
1038
|
+
return await exit(1);
|
|
1001
1039
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
apiKey = result;
|
|
1040
|
+
}
|
|
1041
|
+
if (root !== cwd && !input.root) {
|
|
1042
|
+
const shouldUseRoot = await p7.confirm({
|
|
1043
|
+
message: "do you want to create the prompt file in the git root?",
|
|
1044
|
+
initialValue: true
|
|
1045
|
+
});
|
|
1046
|
+
if (p7.isCancel(shouldUseRoot)) {
|
|
1047
|
+
p7.log.error("aborted");
|
|
1048
|
+
return await exit(1);
|
|
1012
1049
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
llm: {
|
|
1016
|
-
...current2.llm,
|
|
1017
|
-
apiKey
|
|
1018
|
-
}
|
|
1019
|
-
}));
|
|
1020
|
-
p7.log.success(color7.green("noto api key configured!"));
|
|
1021
|
-
await exit(0);
|
|
1050
|
+
if (!shouldUseRoot)
|
|
1051
|
+
promptFile = cwd;
|
|
1022
1052
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
}))
|
|
1053
|
+
const commits = await getCommits();
|
|
1054
|
+
let generate = input.generate;
|
|
1055
|
+
if (generate) {
|
|
1056
|
+
if (!commits || commits.length < 5) {
|
|
1057
|
+
p7.log.error(dedent4`${color7.red("not enough commits to generate a prompt file.")}
|
|
1058
|
+
${color7.gray("at least 5 commits are required.")}`);
|
|
1059
|
+
return await exit(1);
|
|
1060
|
+
}
|
|
1061
|
+
} else if (commits && commits.length >= 5) {
|
|
1062
|
+
const shouldGenerate = await p7.confirm({
|
|
1063
|
+
message: "do you want to generate a prompt file based on existing commits?",
|
|
1064
|
+
initialValue: true
|
|
1036
1065
|
});
|
|
1037
|
-
if (p7.isCancel(
|
|
1038
|
-
p7.log.error(
|
|
1066
|
+
if (p7.isCancel(shouldGenerate)) {
|
|
1067
|
+
p7.log.error("aborted");
|
|
1039
1068
|
return await exit(1);
|
|
1040
1069
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1070
|
+
generate = shouldGenerate;
|
|
1071
|
+
}
|
|
1072
|
+
const spin = p7.spinner();
|
|
1073
|
+
if (commits && generate) {
|
|
1074
|
+
spin.start("generating commit message guidelines");
|
|
1075
|
+
prompt = await generateCommitGuidelines(commits);
|
|
1076
|
+
spin.stop(color7.green("generated commit message guidelines!"));
|
|
1077
|
+
} else {
|
|
1078
|
+
prompt = EMPTY_TEMPLATE;
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
const dir = `${promptFile}/.noto`;
|
|
1082
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
1083
|
+
const filePath = `${dir}/commit-prompt.md`;
|
|
1084
|
+
await fs4.writeFile(filePath, prompt, "utf-8");
|
|
1085
|
+
p7.log.success(dedent4`${color7.green("prompt file created!")}
|
|
1086
|
+
${color7.gray(filePath)}`);
|
|
1087
|
+
return await exit(0);
|
|
1088
|
+
} catch {
|
|
1089
|
+
p7.log.error(color7.red("failed to create the prompt file!"));
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
// src/commands/noto.ts
|
|
1094
|
+
import { z as z8 } from "zod";
|
|
1095
|
+
import * as p8 from "@clack/prompts";
|
|
1096
|
+
import color8 from "picocolors";
|
|
1097
|
+
import clipboard3 from "clipboardy";
|
|
1098
|
+
import { APICallError, RetryError } from "ai";
|
|
1099
|
+
var noto = authedGitProcedure.meta({
|
|
1100
|
+
description: "generate a commit message",
|
|
1101
|
+
default: true,
|
|
1102
|
+
diffRequired: true,
|
|
1103
|
+
promptRequired: true
|
|
1104
|
+
}).input(z8.object({
|
|
1105
|
+
message: z8.string().or(z8.boolean()).meta({
|
|
1106
|
+
description: "provide context for commit message",
|
|
1107
|
+
alias: "m"
|
|
1108
|
+
}),
|
|
1109
|
+
copy: z8.boolean().meta({
|
|
1110
|
+
description: "copy the generated message to clipboard",
|
|
1111
|
+
alias: "c"
|
|
1112
|
+
}),
|
|
1113
|
+
apply: z8.boolean().meta({ description: "commit the generated message", alias: "a" }),
|
|
1114
|
+
push: z8.boolean().meta({ description: "commit and push the changes", alias: "p" }),
|
|
1115
|
+
force: z8.boolean().meta({
|
|
1116
|
+
description: "bypass cache and force regeneration of commit message",
|
|
1117
|
+
alias: "f"
|
|
1118
|
+
}),
|
|
1119
|
+
manual: z8.boolean().meta({ description: "custom commit message" })
|
|
1120
|
+
})).mutation(async (opts) => {
|
|
1121
|
+
const { input, ctx } = opts;
|
|
1122
|
+
const spin = p8.spinner();
|
|
1123
|
+
try {
|
|
1124
|
+
const manual = input.manual;
|
|
1125
|
+
if (manual) {
|
|
1126
|
+
const message2 = await p8.text({
|
|
1127
|
+
message: "edit the generated commit message",
|
|
1128
|
+
placeholder: "chore: init repo"
|
|
1044
1129
|
});
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
1130
|
+
if (p8.isCancel(message2)) {
|
|
1131
|
+
p8.log.error(color8.red("nothing changed!"));
|
|
1047
1132
|
return await exit(1);
|
|
1048
1133
|
}
|
|
1134
|
+
p8.log.step(color8.green(message2));
|
|
1135
|
+
await StorageManager.update((current) => ({
|
|
1136
|
+
...current,
|
|
1137
|
+
lastGeneratedMessage: message2
|
|
1138
|
+
}));
|
|
1139
|
+
const success = await commit(message2);
|
|
1140
|
+
if (success) {
|
|
1141
|
+
p8.log.step(color8.dim("commit successful"));
|
|
1142
|
+
} else {
|
|
1143
|
+
p8.log.error(color8.red("failed to commit changes"));
|
|
1144
|
+
}
|
|
1145
|
+
return await exit(0);
|
|
1049
1146
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1147
|
+
let context = input.message;
|
|
1148
|
+
if (typeof context === "string") {
|
|
1149
|
+
context = context.trim();
|
|
1150
|
+
} else if (typeof context === "boolean" && context === true) {
|
|
1151
|
+
const enteredContext = await p8.text({
|
|
1152
|
+
message: "provide context for the commit message",
|
|
1153
|
+
placeholder: "describe the changes"
|
|
1154
|
+
});
|
|
1155
|
+
if (p8.isCancel(enteredContext)) {
|
|
1156
|
+
p8.log.error(color8.red("nothing changed!"));
|
|
1157
|
+
return await exit(1);
|
|
1055
1158
|
}
|
|
1056
|
-
|
|
1057
|
-
p7.log.success(color7.green("model configured!"));
|
|
1058
|
-
await exit(0);
|
|
1059
|
-
}
|
|
1060
|
-
};
|
|
1061
|
-
var reset = {
|
|
1062
|
-
name: "reset",
|
|
1063
|
-
description: "reset configuration",
|
|
1064
|
-
usage: "noto config reset",
|
|
1065
|
-
execute: async () => {
|
|
1066
|
-
const confirm3 = await p7.confirm({
|
|
1067
|
-
message: "are you sure you want to reset the configuration?"
|
|
1068
|
-
});
|
|
1069
|
-
if (p7.isCancel(confirm3) || !confirm3) {
|
|
1070
|
-
p7.log.error(color7.red("nothing changed!"));
|
|
1071
|
-
return await exit(1);
|
|
1159
|
+
context = enteredContext;
|
|
1072
1160
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
await
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
description: "configure noto",
|
|
1082
|
-
usage: "noto config [subcommand]",
|
|
1083
|
-
execute: async (options) => {
|
|
1084
|
-
const command6 = await p7.select({
|
|
1085
|
-
message: "select a subcommand",
|
|
1086
|
-
options: subCommands.map((cmd2) => ({
|
|
1087
|
-
label: cmd2.description,
|
|
1088
|
-
value: cmd2.name
|
|
1089
|
-
}))
|
|
1161
|
+
spin.start("generating commit message");
|
|
1162
|
+
let message = null;
|
|
1163
|
+
message = await generateCommitMessage(ctx.git.diff, ctx.noto.prompt, typeof context === "string" ? context : undefined, input.force);
|
|
1164
|
+
spin.stop(color8.white(message));
|
|
1165
|
+
const editedMessage = await p8.text({
|
|
1166
|
+
message: "edit the generated commit message",
|
|
1167
|
+
initialValue: message,
|
|
1168
|
+
placeholder: message
|
|
1090
1169
|
});
|
|
1091
|
-
if (
|
|
1170
|
+
if (p8.isCancel(editedMessage)) {
|
|
1171
|
+
p8.log.error(color8.red("nothing changed!"));
|
|
1092
1172
|
return await exit(1);
|
|
1093
1173
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1174
|
+
message = editedMessage;
|
|
1175
|
+
p8.log.step(color8.green(message));
|
|
1176
|
+
await StorageManager.update((current) => ({
|
|
1177
|
+
...current,
|
|
1178
|
+
lastGeneratedMessage: message
|
|
1179
|
+
}));
|
|
1180
|
+
if (input.copy) {
|
|
1181
|
+
clipboard3.writeSync(message);
|
|
1182
|
+
p8.log.step(color8.dim("copied commit message to clipboard"));
|
|
1098
1183
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
// src/commands/help.ts
|
|
1107
|
-
import color8 from "picocolors";
|
|
1108
|
-
var help = {
|
|
1109
|
-
name: "help",
|
|
1110
|
-
description: "show help",
|
|
1111
|
-
usage: "noto help [command]",
|
|
1112
|
-
execute: async (options) => {
|
|
1113
|
-
const command6 = getCommand(options._[0]);
|
|
1114
|
-
if (command6 && command6.name !== "help") {
|
|
1115
|
-
console.log();
|
|
1116
|
-
console.log(color8.bold("usage"));
|
|
1117
|
-
console.log(` ${command6.usage}`);
|
|
1118
|
-
console.log();
|
|
1119
|
-
console.log(color8.bold("description"));
|
|
1120
|
-
console.log(` ${command6.description}`);
|
|
1121
|
-
console.log();
|
|
1122
|
-
} else {
|
|
1123
|
-
const commands2 = listCommand();
|
|
1124
|
-
console.log();
|
|
1125
|
-
console.log(color8.bold("usage"));
|
|
1126
|
-
console.log(` noto [command] [options]`);
|
|
1127
|
-
console.log();
|
|
1128
|
-
console.log(color8.bold("commands"));
|
|
1129
|
-
commands2.forEach((command7) => {
|
|
1130
|
-
console.log(
|
|
1131
|
-
` ${color8.bold(command7.name)} ${color8.dim(command7.description)}`
|
|
1132
|
-
);
|
|
1133
|
-
});
|
|
1134
|
-
await exit(0);
|
|
1184
|
+
if (input.apply) {
|
|
1185
|
+
const success = await commit(message);
|
|
1186
|
+
if (success) {
|
|
1187
|
+
p8.log.step(color8.dim("commit successful"));
|
|
1188
|
+
} else {
|
|
1189
|
+
p8.log.error(color8.red("failed to commit changes"));
|
|
1190
|
+
}
|
|
1135
1191
|
}
|
|
1192
|
+
if (input.push) {
|
|
1193
|
+
const success = await push();
|
|
1194
|
+
if (success) {
|
|
1195
|
+
p8.log.step(color8.dim("push successful"));
|
|
1196
|
+
} else {
|
|
1197
|
+
p8.log.error(color8.red("failed to push changes"));
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return await exit(0);
|
|
1201
|
+
} catch (e) {
|
|
1202
|
+
let msg;
|
|
1203
|
+
if (RetryError.isInstance(e) && APICallError.isInstance(e.lastError)) {
|
|
1204
|
+
msg = safeParseErrorMessage(e.lastError.responseBody);
|
|
1205
|
+
}
|
|
1206
|
+
const suffix = msg ? `
|
|
1207
|
+
${msg}` : "";
|
|
1208
|
+
spin.stop(color8.red(`failed to generate commit message${suffix}`), 1);
|
|
1209
|
+
await exit(1);
|
|
1136
1210
|
}
|
|
1137
|
-
};
|
|
1138
|
-
|
|
1211
|
+
});
|
|
1212
|
+
function safeParseErrorMessage(body) {
|
|
1213
|
+
if (typeof body !== "string")
|
|
1214
|
+
return;
|
|
1215
|
+
try {
|
|
1216
|
+
const parsed = JSON.parse(body);
|
|
1217
|
+
return parsed?.error?.message ?? parsed?.message;
|
|
1218
|
+
} catch {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1139
1222
|
|
|
1140
1223
|
// src/commands/index.ts
|
|
1141
|
-
var commands =
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1224
|
+
var commands = {
|
|
1225
|
+
checkout: checkout2,
|
|
1226
|
+
config,
|
|
1227
|
+
prev,
|
|
1228
|
+
init,
|
|
1229
|
+
noto
|
|
1147
1230
|
};
|
|
1148
1231
|
|
|
1149
|
-
//
|
|
1150
|
-
var
|
|
1232
|
+
// src/router.ts
|
|
1233
|
+
var router = t.router(commands);
|
|
1151
1234
|
|
|
1152
1235
|
// src/index.ts
|
|
1153
|
-
|
|
1154
|
-
"
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
};
|
|
1159
|
-
function main() {
|
|
1160
|
-
const args = process.argv.slice(2);
|
|
1161
|
-
const { command: command6, options: globalOptions } = parse(globalSpec, args);
|
|
1162
|
-
console.log();
|
|
1163
|
-
p8.intro(`${color9.bgCyan(color9.black(" @snelusha/noto "))}`);
|
|
1164
|
-
if (globalOptions["--version"]) return p8.outro(version);
|
|
1165
|
-
if (globalOptions["--help"]) {
|
|
1166
|
-
getCommand("help")?.execute(globalOptions);
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
const cmd = getCommand(command6) ?? getCommand("noto");
|
|
1170
|
-
if (!cmd) return getCommand("noto")?.execute(globalOptions);
|
|
1171
|
-
let commandArgs = args;
|
|
1172
|
-
let selectedCommand = cmd;
|
|
1173
|
-
if (cmd.subCommands && commandArgs.length) {
|
|
1174
|
-
const possibleCommand = commandArgs[1];
|
|
1175
|
-
const subCommand = cmd.subCommands.find(
|
|
1176
|
-
(cmd2) => cmd2.name === possibleCommand || cmd2.aliases && cmd2.aliases.includes(possibleCommand)
|
|
1177
|
-
);
|
|
1178
|
-
if (subCommand) {
|
|
1179
|
-
selectedCommand = subCommand;
|
|
1180
|
-
commandArgs = commandArgs.slice(2);
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
const commandSpec = (selectedCommand.options ?? []).reduce((acc, opt) => {
|
|
1184
|
-
acc[opt.flag] = opt.type ?? Boolean;
|
|
1185
|
-
if (Array.isArray(opt.alias))
|
|
1186
|
-
opt.alias.forEach((alias) => acc[alias] = opt.flag);
|
|
1187
|
-
else if (opt.alias) acc[opt.alias] = opt.flag;
|
|
1188
|
-
return acc;
|
|
1189
|
-
}, {});
|
|
1190
|
-
const { options: commandOptions } = safeParse(commandSpec, commandArgs);
|
|
1191
|
-
const options = { ...globalOptions, ...commandOptions };
|
|
1192
|
-
selectedCommand.execute(options);
|
|
1193
|
-
}
|
|
1194
|
-
main();
|
|
1236
|
+
createCli({
|
|
1237
|
+
name: "noto",
|
|
1238
|
+
router,
|
|
1239
|
+
version
|
|
1240
|
+
}).run();
|