@snelusha/noto 1.2.9 → 1.3.0-beta.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/dist/index.js CHANGED
@@ -1,50 +1,95 @@
1
1
  // src/index.ts
2
- import * as p8 from "@clack/prompts";
3
- import color9 from "picocolors";
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 current = { ...schema };
16
- let iterations = 0;
17
- const maxIterations = Object.keys(current).filter((key) => current[key] === String).length;
18
- while (iterations++ < maxIterations) {
19
- try {
20
- return parse(current, raw);
21
- } catch (error) {
22
- if (error.code === "ARG_MISSING_REQUIRED_LONGARG") {
23
- const match = error.message.match(/(--\w[\w-]*)/);
24
- if (match) {
25
- const missingFlag = match[0];
26
- if (current[missingFlag] === String) {
27
- current[missingFlag] = Boolean;
28
- continue;
29
- }
30
- }
31
- }
32
- throw error;
33
- }
34
- }
35
- return parse(current, raw);
36
- };
37
-
38
- // src/commands/noto.ts
39
- import * as p3 from "@clack/prompts";
40
- import color3 from "picocolors";
41
- import clipboard from "clipboardy";
2
+ import { createCli } from "trpc-cli";
3
+ // package.json
4
+ var version = "1.3.0-beta.1";
42
5
 
43
- // src/middleware/auth.ts
6
+ // src/trpc.ts
7
+ import fs3 from "node:fs/promises";
8
+ import { initTRPC } from "@trpc/server";
44
9
  import * as p from "@clack/prompts";
45
10
  import color from "picocolors";
46
11
  import dedent from "dedent";
47
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
+
48
93
  // src/utils/storage.ts
49
94
  import os from "os";
50
95
  import { dirname, join, resolve } from "path";
@@ -73,11 +118,12 @@ var StorageSchema = z2.object({
73
118
  apiKey: z2.string().optional(),
74
119
  model: AvailableModelsSchema.optional().or(z2.string())
75
120
  }).optional(),
76
- lastGeneratedMessage: z2.string().optional()
121
+ lastGeneratedMessage: z2.string().optional(),
122
+ cache: z2.record(z2.string(), z2.string()).optional()
77
123
  });
78
124
 
79
125
  class StorageManager {
80
- static storagePath = resolve(join(os.homedir(), ".noto"), "storage.sithi");
126
+ static storagePath = resolve(join(os.homedir(), ".config", "noto"), ".notorc");
81
127
  static storage = {};
82
128
  static async load() {
83
129
  try {
@@ -123,6 +169,38 @@ class StorageManager {
123
169
  }
124
170
  }
125
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
+ });
202
+ };
203
+
126
204
  // src/utils/process.ts
127
205
  var exit = async (code) => {
128
206
  await new Promise((resolve2) => setTimeout(resolve2, 1));
@@ -130,149 +208,230 @@ var exit = async (code) => {
130
208
  process.exit(code);
131
209
  };
132
210
 
133
- // src/middleware/auth.ts
134
- var withAuth = (fn, options = { enabled: true }) => {
135
- return async (opts) => {
136
- const storage = await StorageManager.get();
137
- const apiKey = process.env.NOTO_API_KEY || storage.llm?.apiKey;
138
- if (!apiKey && options.enabled) {
139
- p.log.error(dedent`${color.red("noto api key is missing.")}
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.")}
140
227
  ${color.dim(`run ${color.cyan("`noto config key`")} to set it up.`)}`);
141
- return await exit(1);
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 {}
142
253
  }
143
- return fn(opts);
144
- };
145
- };
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);
146
278
 
147
- // src/middleware/git.ts
279
+ // src/commands/checkout.ts
280
+ import { z as z3 } from "zod";
148
281
  import * as p2 from "@clack/prompts";
149
282
  import color2 from "picocolors";
150
- import dedent2 from "dedent";
151
-
152
- // src/utils/git.ts
153
- import simpleGit from "simple-git";
154
- var INIT_COMMIT_MESSAGE = "chore: init repo";
155
- var git = simpleGit();
156
- var isGitRepository = async () => {
157
- return git.checkIsRepo();
158
- };
159
- var getCommitCount = async () => {
160
- try {
161
- const count = await git.raw(["rev-list", "--all", "--count"]);
162
- return parseInt(count);
163
- } catch (error) {
164
- const message = error.message;
165
- const regex = /(ambiguous argument.*HEAD|unknown revision or path.*HEAD)/;
166
- if (regex.test(message))
167
- return 0;
168
- 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);
169
299
  }
170
- };
171
- var isFirstCommit = async () => {
172
- const count = await getCommitCount();
173
- return count === 0;
174
- };
175
- var getStagedDiff = async () => {
176
- try {
177
- return git.diff(["--cached", "--", ":!*.lock"]);
178
- } catch {
179
- return null;
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);
180
315
  }
181
- };
182
- var commit = async (message, amend) => {
183
- try {
184
- const options = amend ? { "--amend": null } : undefined;
185
- const {
186
- summary: { changes }
187
- } = await git.commit(message, undefined, options);
188
- return Boolean(changes);
189
- } catch {
190
- return false;
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);
191
348
  }
192
- };
193
- var push = async () => {
194
- try {
195
- const result = await git.push();
196
- return result.update || result.pushed && result.pushed.length > 0;
197
- } catch {
198
- return false;
349
+ if (branches.length === 0) {
350
+ p2.log.error("no branches found in the repository");
351
+ return await exit(1);
199
352
  }
200
- };
201
- var getCurrentBranch = async () => {
202
- try {
203
- const branch = await git.branch();
204
- return branch.current;
205
- } catch {
206
- return null;
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);
207
365
  }
208
- };
209
- var getBranches = async (remote) => {
210
- try {
211
- const branches = await git.branch();
212
- return remote ? branches.all : Object.keys(branches.branches).filter((b) => !b.startsWith("remotes/"));
213
- } catch {
214
- return null;
366
+ if (!branch) {
367
+ p2.log.error("no branch selected");
368
+ return await exit(1);
215
369
  }
216
- };
217
- var checkout = async (branch) => {
218
- try {
219
- await git.checkout(branch, {});
220
- return true;
221
- } catch {
222
- 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);
223
374
  }
224
- };
225
- var checkoutLocalBranch = async (branch) => {
226
- try {
227
- await git.checkoutLocalBranch(branch);
228
- return true;
229
- } catch {
230
- return false;
375
+ if (branch === currentBranch) {
376
+ p2.log.error(`${color2.red("already on branch")}`);
377
+ return await exit(1);
231
378
  }
232
- };
233
- var deleteBranches = async (branches, force = false) => {
234
- try {
235
- const result = await git.deleteLocalBranches(branches, force);
236
- return result.success;
237
- } catch {
238
- 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);
239
383
  }
240
- };
384
+ p2.log.success(`checked out ${color2.green(branch)}`);
385
+ await exit(0);
386
+ });
241
387
 
242
- // src/middleware/git.ts
243
- var withRepository = (fn, options = { enabled: true }) => {
244
- return async (opts) => {
245
- const isRepo = await isGitRepository();
246
- if (!isRepo && options.enabled) {
247
- p2.log.error(dedent2`${color2.red("no git repository found in cwd.")}
248
- ${color2.dim(`run ${color2.cyan("`git init`")} to initialize a new repository.`)}`);
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!"));
249
401
  return await exit(1);
250
402
  }
251
- opts.isRepo = isRepo;
252
- if (isRepo) {
253
- const diff = await getStagedDiff();
254
- if (!diff && options.enabled) {
255
- p2.log.error(dedent2`${color2.red("no staged changes found.")}
256
- ${color2.dim(`run ${color2.cyan("`git add <file>`")} or ${color2.cyan("`git add .`")} to stage changes.`)}`);
257
- return await exit(1);
258
- }
259
- 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);
260
411
  }
261
- return fn(opts);
262
- };
263
- };
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
+ });
264
424
 
265
- // src/ai/index.ts
266
- import { generateObject } from "ai";
267
- import z3 from "zod";
268
- import dedent3 from "dedent";
425
+ // src/commands/config/model.ts
426
+ import * as p4 from "@clack/prompts";
427
+ import color4 from "picocolors";
269
428
 
270
429
  // src/ai/models.ts
271
430
  import { createGoogleGenerativeAI } from "@ai-sdk/google";
272
431
  var google = createGoogleGenerativeAI({
273
432
  apiKey: (await StorageManager.get()).llm?.apiKey ?? "api-key"
274
433
  });
275
- var defaultModel = "gemini-2.0-flash";
434
+ var DEFAULT_MODEL = "gemini-2.0-flash";
276
435
  var models = {
277
436
  "gemini-1.5-flash": google("gemini-1.5-flash"),
278
437
  "gemini-1.5-flash-latest": google("gemini-1.5-flash-latest"),
@@ -290,839 +449,792 @@ var availableModels = Object.keys(models);
290
449
  var getModel = async () => {
291
450
  let model = (await StorageManager.get()).llm?.model;
292
451
  if (!model || !availableModels.includes(model)) {
293
- model = defaultModel;
452
+ model = DEFAULT_MODEL;
294
453
  await StorageManager.update((current) => ({
295
454
  ...current,
296
455
  llm: {
297
456
  ...current.llm,
298
- model: defaultModel
457
+ model: DEFAULT_MODEL
299
458
  }
300
459
  }));
301
460
  }
302
461
  return models[model];
303
462
  };
304
463
 
305
- // src/ai/index.ts
306
- var generateCommitMessage = async (diff, type, context) => {
307
- const model = await getModel();
308
- const { object } = await generateObject({
309
- model,
310
- schema: z3.object({
311
- message: z3.string()
312
- }),
313
- messages: [
314
- {
315
- role: "system",
316
- content: dedent3`
317
- ## Persona
318
- You are a highly specialized AI assistant engineered for generating precise, standards-compliant Git commit messages. Your function is to serve as an automated tool ensuring consistency and clarity in version control history, adhering strictly to predefined formatting rules optimized for software development workflows.
319
-
320
- ## Core Objective
321
- Your objective is to meticulously analyze provided code changes (diffs) and synthesize a Git commit message that strictly conforms to the specified output requirements. The focus is on accuracy, conciseness, and unwavering adherence to the format.
322
-
323
- ## Input Specification
324
- The input provided by the user will be structured as follows:
325
-
326
- 1. **Optional Type Override:** A line indicating the desired commit type, formatted as:
327
- \`USER_SPECIFIED_TYPE: <value>\`
328
- * The \`<value>\` will either be one of the valid types (\`chore\`, \`feat\`, \`fix\`, \`docs\`, \`refactor\`, \`perf\`, \`test\) or the special keyword \`[none]\` if the user does not wish to force a specific type.
329
- 2. **Optional User Context:** A section providing supplementary information about the changes, formatted as:
330
- \`USER_PROVIDED_CONTEXT:\`
331
- \`{context_placeholder or [none]}\`
332
- * This context (e.g., issue details, goal description) is optional. If provided, use it to better understand the *purpose* and *intent* behind the code changes. If not provided, the value will be \`[none]\`.
333
- 3. **Diff Content:** The raw diff output, typically generated by \`git diff --staged\`, encapsulated within triple backticks.
334
-
335
- **You must parse the \`USER_SPECIFIED_TYPE:\` and \`USER_PROVIDED_CONTEXT:\` inputs before analyzing the diff.**
336
-
337
- ## Output Specification & Constraints (Mandatory Adherence)
338
- Adherence to the following specifications is mandatory and non-negotiable. Any deviation constitutes an incorrect output.
339
-
340
- 1. **Commit Message Format:**
341
- * The output MUST strictly follow the single-line format: \`<type>: <description>\`
342
- * A single colon (\`:\`) followed by a single space MUST separate the \`<type>\` and \`<description>\`.
343
- * Scopes within the type (e.g., \`feat(api):\`) are explicitly disallowed.
344
- * Message bodies and footers are explicitly disallowed. The output MUST be exactly one line.
345
-
346
- 2. **Type (\`<type>\`) - Determination Logic:**
347
- * **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.
348
- * **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.
349
-
350
- 3. **Description (\`<description>\`):**
351
- * **Tense:** MUST employ the imperative, present tense (e.g.,\`add\`, \`fix\`, \`update\`, \`implement\`, \`refactor\`, \`remove\`).
352
- * **Content:** Must succinctly convey the core semantic change introduced by the diff.
353
- * **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.
354
- * **Focus:** Prioritize the most significant aspects of the change.
355
- * **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.
356
- * **Case Sensitivity:** The entire output string, encompassing both \`<type>\` and \`<description>\`, MUST be rendered in lowercase.
357
- * **Punctuation:** The description MUST NOT conclude with any terminal punctuation (e.g., no period/full stop).
358
- * **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.**`
359
- },
360
- {
361
- role: "user",
362
- content: dedent3`
363
- \`\`\`text
364
- USER_SPECIFIED_TYPE: ${type ?? "[none]"}
365
-
366
- USER_PROVIDED_CONTEXT:
367
- ${context ?? "[none]"}
368
- \`\`\`
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
+ });
369
499
 
370
- \`\`\`diff
371
- ${diff}
372
- \`\`\``
373
- }
374
- ]
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?"
375
508
  });
376
- return object.message.trim();
377
- };
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
+ });
378
517
 
379
- // src/commands/noto.ts
380
- var availableTypes = [
381
- "chore",
382
- "feat",
383
- "fix",
384
- "docs",
385
- "refactor",
386
- "perf",
387
- "test"
388
- ];
389
- var commitTypeOptions = availableTypes.map((type) => ({
390
- label: type,
391
- value: type
392
- }));
393
- var command = {
394
- name: "noto",
395
- description: "generate commit message",
396
- usage: "noto [options]",
397
- options: [
398
- {
399
- type: String,
400
- flag: "--type",
401
- alias: "-t",
402
- description: "generate commit message based on type"
403
- },
404
- {
405
- type: String,
406
- flag: "--message",
407
- alias: "-m",
408
- description: "provide context for the commit message"
409
- },
410
- {
411
- type: Boolean,
412
- flag: "--copy",
413
- alias: "-c",
414
- description: "copy the generated commit message to clipboard"
415
- },
416
- {
417
- type: Boolean,
418
- flag: "--apply",
419
- alias: "-a",
420
- description: "commit the generated message directly"
421
- },
422
- {
423
- type: Boolean,
424
- flag: "--push",
425
- alias: "-p",
426
- description: "commit and push the changes"
427
- },
428
- {
429
- type: Boolean,
430
- flag: "--manual",
431
- description: "commit and push the changes"
432
- }
433
- ],
434
- execute: withAuth(withRepository(async (options) => {
435
- const spin = p3.spinner();
436
- try {
437
- const { diff } = options;
438
- const manual = options["--manual"];
439
- if (manual) {
440
- const message2 = await p3.text({
441
- message: "edit the generated commit message",
442
- placeholder: "chore: init repo"
443
- });
444
- if (p3.isCancel(message2)) {
445
- p3.log.error(color3.red("nothing changed!"));
446
- return await exit(1);
447
- }
448
- p3.log.step(color3.green(message2));
449
- await StorageManager.update((current) => ({
450
- ...current,
451
- lastGeneratedMessage: message2
452
- }));
453
- if (options["--apply"]) {
454
- const success = await commit(message2);
455
- if (success) {
456
- p3.log.step(color3.dim("commit successful"));
457
- } else {
458
- p3.log.error(color3.red("failed to commit changes"));
459
- }
460
- }
461
- return await exit(0);
462
- }
463
- const type = options["--type"];
464
- if (typeof type === "string" && !availableTypes.includes(type) || typeof type === "boolean") {
465
- const type2 = await p3.select({
466
- message: "select the type of commit message",
467
- options: commitTypeOptions
468
- });
469
- if (p3.isCancel(type2)) {
470
- p3.log.error(color3.red("nothing selected!"));
471
- return await exit(1);
472
- }
473
- options.type = type2;
474
- } else if (typeof type === "string") {
475
- options.type = type;
476
- }
477
- const context = options["--message"];
478
- if (typeof context === "string") {
479
- options.context = context;
480
- } else if (typeof context === "boolean") {
481
- const context2 = await p3.text({
482
- message: "provide context for the commit message",
483
- placeholder: "describe the changes"
484
- });
485
- if (p3.isCancel(context2)) {
486
- p3.log.error(color3.red("nothing changed!"));
487
- return await exit(1);
488
- }
489
- options.context = context2;
490
- }
491
- spin.start("generating commit message");
492
- let message = null;
493
- if (!await isFirstCommit()) {
494
- message = await generateCommitMessage(diff, options.type, options.context);
495
- } else {
496
- message = INIT_COMMIT_MESSAGE;
497
- }
498
- spin.stop(color3.white(message));
499
- const editedMessage = await p3.text({
500
- message: "edit the generated commit message",
501
- initialValue: message,
502
- placeholder: message
503
- });
504
- if (p3.isCancel(editedMessage)) {
505
- p3.log.error(color3.red("nothing changed!"));
506
- return await exit(1);
507
- }
508
- message = editedMessage;
509
- p3.log.step(color3.green(message));
510
- await StorageManager.update((current) => ({
511
- ...current,
512
- lastGeneratedMessage: message
513
- }));
514
- if (options["--copy"]) {
515
- clipboard.writeSync(message);
516
- p3.log.step(color3.dim("copied commit message to clipboard"));
517
- }
518
- if (options["--apply"]) {
519
- const success = await commit(message);
520
- if (success) {
521
- p3.log.step(color3.dim("commit successful"));
522
- } else {
523
- p3.log.error(color3.red("failed to commit changes"));
524
- }
525
- }
526
- if (options["--push"]) {
527
- const success = await push();
528
- if (success) {
529
- p3.log.step(color3.dim("push successful"));
530
- } else {
531
- p3.log.error(color3.red("failed to push changes"));
532
- }
533
- }
534
- return await exit(0);
535
- } catch {
536
- spin.stop(color3.red("failed to generate commit message"), 1);
537
- return await exit(1);
538
- }
539
- }))
540
- };
541
- var noto_default = command;
518
+ // src/commands/config/index.ts
519
+ var config = t.router({
520
+ key,
521
+ model,
522
+ reset
523
+ });
542
524
 
543
525
  // src/commands/prev.ts
544
- import * as p4 from "@clack/prompts";
545
- import color4 from "picocolors";
546
- import dedent4 from "dedent";
526
+ import { z as z5 } from "zod";
527
+ import * as p6 from "@clack/prompts";
528
+ import color6 from "picocolors";
529
+ import dedent2 from "dedent";
547
530
  import clipboard2 from "clipboardy";
548
- var command2 = {
549
- name: "prev",
550
- description: "access the last generated commit message",
551
- usage: "noto prev [options]",
552
- options: [
553
- {
554
- type: Boolean,
555
- flag: "--copy",
556
- alias: "-c",
557
- description: "copy the last generated commit message to clipboard"
558
- },
559
- {
560
- type: Boolean,
561
- flag: "--apply",
562
- alias: "-a",
563
- description: "commit the last generated message directly"
564
- },
565
- {
566
- type: Boolean,
567
- flag: "--edit",
568
- alias: "-e",
569
- description: "edit the last generated commit message"
570
- },
571
- {
572
- type: Boolean,
573
- flag: "--amend",
574
- description: "amend the last commit with the last generated message"
575
- }
576
- ],
577
- execute: withAuth(withRepository(async (options) => {
578
- let lastGeneratedMessage = (await StorageManager.get()).lastGeneratedMessage;
579
- if (!lastGeneratedMessage) {
580
- p4.log.error(color4.red("no previous commit message found"));
581
- return await exit(1);
582
- }
583
- const isEditMode = options["--edit"];
584
- const isAmend = options["--amend"];
585
- if (isAmend && !isEditMode) {
586
- p4.log.error(color4.red("the --amend option requires the --edit option"));
587
- return await exit(1);
588
- }
589
- p4.log.step(isEditMode ? color4.white(lastGeneratedMessage) : color4.green(lastGeneratedMessage));
590
- if (options["--edit"]) {
591
- const editedMessage = await p4.text({
592
- message: "edit the last generated commit message",
593
- initialValue: lastGeneratedMessage,
594
- placeholder: lastGeneratedMessage
595
- });
596
- if (p4.isCancel(editedMessage)) {
597
- p4.log.error(color4.red("nothing changed!"));
598
- return await exit(1);
599
- }
600
- lastGeneratedMessage = editedMessage;
601
- await StorageManager.update((current) => ({
602
- ...current,
603
- lastGeneratedMessage: editedMessage
604
- }));
605
- p4.log.step(color4.green(lastGeneratedMessage));
606
- }
607
- if (options["--copy"]) {
608
- clipboard2.writeSync(lastGeneratedMessage);
609
- p4.log.step(color4.dim("copied last generated commit message to clipboard"));
610
- }
611
- if (options["--apply"] || isAmend) {
612
- if (!options.isRepo) {
613
- p4.log.error(dedent4`${color4.red("no git repository found in cwd.")}
614
- ${color4.dim(`run ${color4.cyan("`git init`")} to initialize a new repository.`)}`);
615
- return await exit(1);
616
- }
617
- if (!options.diff && !isAmend) {
618
- p4.log.error(dedent4`${color4.red("no staged changes found.")}
619
- ${color4.dim(`run ${color4.cyan("`git add <file>`")} or ${color4.cyan("`git add .`")} to stage changes.`)}`);
620
- return await exit(1);
621
- }
622
- const success = await commit(lastGeneratedMessage, isAmend);
623
- if (success) {
624
- p4.log.step(color4.dim("commit successful"));
625
- } else {
626
- p4.log.error(color4.red("failed to commit changes"));
627
- }
628
- }
629
- return await exit(0);
630
- }, { enabled: false }))
631
- };
632
- var prev_default = command2;
633
-
634
- // src/commands/branch.ts
635
- import * as p5 from "@clack/prompts";
636
- import color5 from "picocolors";
637
- import clipboard3 from "clipboardy";
638
- import dedent5 from "dedent";
639
- var current = {
640
- name: "current",
641
- description: "get current branch",
642
- usage: "branch current",
643
- options: [
644
- {
645
- type: Boolean,
646
- flag: "--copy",
647
- alias: "-c",
648
- description: "copy the selected branch to clipboard"
649
- }
650
- ],
651
- execute: withRepository(async (options) => {
652
- if (!options.isRepo) {
653
- p5.log.error(dedent5`${color5.red("no git repository found in cwd.")}
654
- ${color5.dim(`run ${color5.cyan("`git init`")} to initialize a new repository.`)}`);
655
- return await exit(1);
656
- }
657
- const branch = await getCurrentBranch();
658
- if (!branch) {
659
- p5.log.error("failed to fetch current branch");
660
- return await exit(1);
661
- }
662
- p5.log.success(`current branch: ${color5.bold(branch)}`);
663
- if (options["--copy"]) {
664
- clipboard3.writeSync(branch);
665
- p5.log.success(`${color5.green("copied to clipboard!")}`);
666
- }
667
- await exit(0);
668
- }, { enabled: false })
669
- };
670
- var del = {
671
- name: "delete",
672
- description: "delete a branch",
673
- usage: "branch delete <branch>",
674
- options: [
675
- {
676
- type: Boolean,
677
- flag: "--force",
678
- alias: "-f",
679
- description: "force delete a branch"
680
- },
681
- {
682
- type: Boolean,
683
- flag: "--all",
684
- alias: "-a",
685
- description: "select all branches except the current one"
686
- }
687
- ],
688
- execute: withRepository(async (options) => {
689
- if (!options.isRepo) {
690
- p5.log.error(dedent5`${color5.red("no git repository found in cwd.")}
691
- ${color5.dim(`run ${color5.cyan("`git init`")} to initialize a new repository.`)}`);
692
- return await exit(1);
693
- }
694
- const currentBranch = await getCurrentBranch();
695
- const branches = await getBranches();
696
- if (!currentBranch || !branches) {
697
- p5.log.error("failed to fetch branches");
698
- return await exit(1);
699
- }
700
- const selectedBranches = await p5.multiselect({
701
- message: "select branches to delete",
702
- initialValues: options["--all"] ? branches.filter((b) => b !== currentBranch) : [],
703
- options: branches.map((branch) => ({
704
- value: branch,
705
- label: color5.bold(branch),
706
- hint: branch === options["--current"] ? "current branch" : undefined
707
- }))
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
708
563
  });
709
- if (p5.isCancel(selectedBranches)) {
710
- p5.log.error("nothing selected!");
564
+ if (p6.isCancel(editedMessage)) {
565
+ p6.log.error(color6.red("nothing changed!"));
711
566
  return await exit(1);
712
567
  }
713
- if (!selectedBranches) {
714
- p5.log.error("no branch selected");
715
- return await exit(1);
716
- }
717
- const force = options["--force"];
718
- if (currentBranch && selectedBranches.includes(currentBranch)) {
719
- p5.log.error("cannot delete current branch");
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.`)}`);
720
583
  return await exit(1);
721
584
  }
722
- const deletedBranches = await deleteBranches(selectedBranches, force);
723
- if (!deletedBranches) {
724
- p5.log.error("failed to delete branches");
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.`)}`);
725
588
  return await exit(1);
726
589
  }
727
- p5.log.success("branches deleted successfully");
728
- await exit(0);
729
- }, { enabled: false })
730
- };
731
- var command3 = {
732
- name: "branch",
733
- description: "list branches",
734
- usage: "branch [options]",
735
- options: [
736
- {
737
- type: Boolean,
738
- flag: "--remote",
739
- alias: "-r",
740
- description: "list branches including remotes"
741
- },
742
- {
743
- type: Boolean,
744
- flag: "--delete",
745
- alias: "-d",
746
- description: "delete a branch"
747
- },
748
- {
749
- type: Boolean,
750
- flag: "--force",
751
- alias: "-f",
752
- description: "force delete a branch"
753
- },
754
- {
755
- type: Boolean,
756
- flag: "--all",
757
- alias: "-a",
758
- description: "select all branches except the current one"
759
- }
760
- ],
761
- execute: withRepository(async (options) => {
762
- if (!options.isRepo) {
763
- p5.log.error(dedent5`${color5.red("no git repository found in cwd.")}
764
- ${color5.dim(`run ${color5.cyan("`git init`")} to initialize a new repository.`)}`);
765
- return await exit(1);
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"));
766
595
  }
767
- if (options["--delete"])
768
- return del.execute(options);
769
- const remote = options["--remote"];
770
- const branches = await getBranches(remote);
771
- if (!branches) {
772
- p5.log.error("failed to fetch branches");
773
- return await exit(1);
596
+ }
597
+ return await exit(0);
598
+ });
599
+
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);
774
931
  }
775
- const currentBranch = await getCurrentBranch();
776
- const branch = await p5.select({
777
- message: "select a branch",
778
- options: branches.map((branch2) => ({
779
- value: branch2,
780
- label: color5.bold(branch2 === currentBranch ? color5.green(branch2) : branch2),
781
- hint: branch2 === currentBranch ? "current branch" : undefined
782
- })),
783
- initialValue: currentBranch
932
+ const result = await doGenerate();
933
+ await StorageManager.update((current) => {
934
+ return {
935
+ ...current,
936
+ cache: {
937
+ [key2]: superjson.stringify(result)
938
+ }
939
+ };
784
940
  });
785
- if (p5.isCancel(branch)) {
786
- p5.log.error("nothing selected!");
787
- return await exit(1);
788
- }
789
- if (!branch) {
790
- p5.log.error("no branch selected");
791
- return await exit(1);
792
- }
793
- clipboard3.writeSync(branch);
794
- p5.log.success(`${color5.green("copied to clipboard!")}`);
795
- await exit(0);
796
- }, { enabled: false }),
797
- subCommands: [current, del]
941
+ return result;
942
+ }
798
943
  };
799
- var branch_default = command3;
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}` : ""}
800
967
 
801
- // src/commands/checkout.ts
802
- import * as p6 from "@clack/prompts";
803
- import color6 from "picocolors";
804
- import clipboard4 from "clipboardy";
805
- import dedent6 from "dedent";
806
- var command4 = {
807
- name: "checkout",
808
- description: "checkout a branch",
809
- usage: "checkout [options]",
810
- options: [
811
- {
812
- type: Boolean,
813
- flag: "--copy",
814
- alias: "-c",
815
- description: "copy the selected branch to clipboard"
816
- },
817
- {
818
- type: Boolean,
819
- flag: "--create",
820
- alias: "-b",
821
- description: "create a new branch"
822
- }
823
- ],
824
- execute: withRepository(async (options) => {
825
- const args = options._.slice(1);
826
- if (!options.isRepo) {
827
- p6.log.error(dedent6`${color6.red("no git repository found in cwd.")}
828
- ${color6.dim(`run ${color6.cyan("`git init`")} to initialize a new repository.`)}`);
829
- return await exit(1);
830
- }
831
- const branches = await getBranches();
832
- if (!branches) {
833
- p6.log.error("failed to fetch branches");
834
- return await exit(1);
835
- }
836
- const currentBranch = await getCurrentBranch();
837
- const branchName = args[0];
838
- if (options["--create"]) {
839
- if (branches.includes(branchName)) {
840
- p6.log.error(`branch ${color6.red(branchName)} already exists in the repository`);
841
- return await exit(1);
842
- }
843
- const result2 = await checkoutLocalBranch(branchName);
844
- if (!result2) {
845
- p6.log.error(`failed to create and checkout ${color6.bold(branchName)}`);
846
- return await exit(1);
847
- }
848
- p6.log.success(`created and checked out ${color6.green(branchName)}`);
849
- return await exit(0);
850
- }
851
- if (branchName) {
852
- if (!branches.includes(branchName)) {
853
- p6.log.error(`branch ${color6.red(branchName)} does not exist in the repository`);
854
- const createBranch = await p6.confirm({
855
- message: `do you want to create branch ${color6.green(branchName)}?`
856
- });
857
- if (p6.isCancel(createBranch)) {
858
- p6.log.error("aborted");
859
- return await exit(1);
860
- }
861
- if (createBranch) {
862
- const result3 = await checkoutLocalBranch(branchName);
863
- if (!result3) {
864
- p6.log.error(`failed to create and checkout ${color6.bold(branchName)}`);
865
- return await exit(1);
866
- }
867
- p6.log.success(`created and checked out ${color6.green(branchName)}`);
868
- return await exit(0);
869
- }
870
- return await exit(1);
968
+ GIT DIFF:
969
+ ${diff}
970
+ `
871
971
  }
872
- if (branchName === currentBranch) {
873
- p6.log.error(`${color6.red("already on branch")} ${color6.green(branchName)}`);
874
- return await exit(1);
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
+ `)}`
875
994
  }
876
- const result2 = await checkout(branchName);
877
- if (!result2) {
878
- p6.log.error(`failed to checkout ${color6.bold(branchName)}`);
995
+ ]
996
+ });
997
+ return object.prompt.trim();
998
+ };
999
+
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 = authedGitProcedure.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
1029
+ });
1030
+ if (p7.isCancel(shouldContinue) || !shouldContinue) {
1031
+ p7.log.error("aborted");
879
1032
  return await exit(1);
880
1033
  }
881
- p6.log.success(`checked out ${color6.green(branchName)}`);
882
- return await exit(0);
883
- }
884
- const branch = await p6.select({
885
- message: "select a branch to checkout",
886
- options: branches.map((branch2) => ({
887
- value: branch2,
888
- label: color6.bold(branch2 === currentBranch ? color6.green(branch2) : branch2),
889
- hint: branch2 === currentBranch ? "current branch" : undefined
890
- })),
891
- initialValue: currentBranch
892
- });
893
- if (p6.isCancel(branch)) {
894
- p6.log.error("nothing selected!");
1034
+ promptFile = cwd;
1035
+ } else {
1036
+ p7.log.error(dedent4`${color7.red("a prompt file already exists.")}
1037
+ ${color7.gray(existingPromptFile)}`);
895
1038
  return await exit(1);
896
1039
  }
897
- if (!branch) {
898
- p6.log.error("no branch selected");
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");
899
1048
  return await exit(1);
900
1049
  }
901
- if (options["--copy"]) {
902
- clipboard4.writeSync(branch);
903
- p6.log.success(`copied ${color6.green(branch)} to clipboard`);
904
- return await exit(0);
905
- }
906
- if (branch === currentBranch) {
907
- p6.log.error(`${color6.red("already on branch")}`);
1050
+ if (!shouldUseRoot)
1051
+ promptFile = cwd;
1052
+ }
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.")}`);
908
1059
  return await exit(1);
909
1060
  }
910
- const result = await checkout(branch);
911
- if (!result) {
912
- p6.log.error(`failed to checkout ${color6.bold(branch)}`);
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
1065
+ });
1066
+ if (p7.isCancel(shouldGenerate)) {
1067
+ p7.log.error("aborted");
913
1068
  return await exit(1);
914
1069
  }
915
- p6.log.success(`checked out ${color6.green(branch)}`);
916
- await exit(0);
917
- }, { enabled: false })
918
- };
919
- var checkout_default = command4;
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
+ });
920
1092
 
921
- // src/commands/config.ts
922
- import * as p7 from "@clack/prompts";
923
- import color7 from "picocolors";
924
- var key = {
925
- name: "key",
926
- description: "configure api key",
927
- usage: "noto config key [options]",
928
- execute: async (options) => {
929
- if ((await StorageManager.get()).llm?.apiKey) {
930
- const confirm3 = await p7.confirm({
931
- message: "noto api key already configured, do you want to update it?"
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"
932
1129
  });
933
- if (p7.isCancel(confirm3) || !confirm3) {
934
- p7.log.error(color7.red("nothing changed!"));
1130
+ if (p8.isCancel(message2)) {
1131
+ p8.log.error(color8.red("nothing changed!"));
935
1132
  return await exit(1);
936
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);
937
1146
  }
938
- let apiKey = options._[0];
939
- if (!apiKey) {
940
- const result = await p7.text({
941
- message: "enter your noto api key"
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"
942
1154
  });
943
- if (p7.isCancel(result)) {
944
- p7.log.error(color7.red("nothing changed!"));
1155
+ if (p8.isCancel(enteredContext)) {
1156
+ p8.log.error(color8.red("nothing changed!"));
945
1157
  return await exit(1);
946
1158
  }
947
- apiKey = result;
1159
+ context = enteredContext;
948
1160
  }
949
- await StorageManager.update((current2) => ({
950
- ...current2,
951
- llm: {
952
- ...current2.llm,
953
- apiKey
954
- }
955
- }));
956
- p7.log.success(color7.green("noto api key configured!"));
957
- await exit(0);
958
- }
959
- };
960
- var model = {
961
- name: "model",
962
- description: "configure model",
963
- usage: "noto config model [options]",
964
- execute: async () => {
965
- const model2 = await p7.select({
966
- message: "select a model",
967
- initialValue: (await StorageManager.get()).llm?.model,
968
- options: Object.keys(models).map((model3) => ({
969
- label: model3,
970
- value: model3
971
- }))
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
972
1169
  });
973
- if (p7.isCancel(model2)) {
974
- p7.log.error(color7.red("nothing changed!"));
1170
+ if (p8.isCancel(editedMessage)) {
1171
+ p8.log.error(color8.red("nothing changed!"));
975
1172
  return await exit(1);
976
1173
  }
977
- if (model2 === "gemini-2.5-pro-preview-05-06") {
978
- const confirm3 = await p7.confirm({
979
- message: "this model does not have free quota tier, do you want to continue?"
980
- });
981
- if (p7.isCancel(confirm3) || !confirm3) {
982
- p7.log.error(color7.red("nothing changed!"));
983
- return await exit(1);
984
- }
985
- }
986
- await StorageManager.update((current2) => ({
987
- ...current2,
988
- llm: {
989
- ...current2.llm,
990
- model: model2
991
- }
1174
+ message = editedMessage;
1175
+ p8.log.step(color8.green(message));
1176
+ await StorageManager.update((current) => ({
1177
+ ...current,
1178
+ lastGeneratedMessage: message
992
1179
  }));
993
- p7.log.success(color7.green("model configured!"));
994
- await exit(0);
995
- }
996
- };
997
- var reset = {
998
- name: "reset",
999
- description: "reset configuration",
1000
- usage: "noto config reset",
1001
- execute: async () => {
1002
- const confirm3 = await p7.confirm({
1003
- message: "are you sure you want to reset the configuration?"
1004
- });
1005
- if (p7.isCancel(confirm3) || !confirm3) {
1006
- p7.log.error(color7.red("nothing changed!"));
1007
- return await exit(1);
1180
+ if (input.copy) {
1181
+ clipboard3.writeSync(message);
1182
+ p8.log.step(color8.dim("copied commit message to clipboard"));
1008
1183
  }
1009
- await StorageManager.clear();
1010
- p7.log.success(color7.green("configuration reset!"));
1011
- await exit(0);
1012
- }
1013
- };
1014
- var subCommands = [key, model, reset];
1015
- var command5 = {
1016
- name: "config",
1017
- description: "configure noto",
1018
- usage: "noto config [subcommand]",
1019
- execute: async (options) => {
1020
- const command6 = await p7.select({
1021
- message: "select a subcommand",
1022
- options: subCommands.map((cmd2) => ({
1023
- label: cmd2.description,
1024
- value: cmd2.name
1025
- }))
1026
- });
1027
- if (p7.isCancel(command6)) {
1028
- return await exit(1);
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
+ }
1029
1191
  }
1030
- const cmd = getCommand(command6, subCommands);
1031
- if (!cmd) {
1032
- p7.log.error(color7.red("unknown config command"));
1033
- return await exit(1);
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
+ }
1034
1199
  }
1035
- options._ = options._.slice(1);
1036
- cmd.execute(options);
1037
- },
1038
- subCommands
1039
- };
1040
- var config_default = command5;
1041
-
1042
- // src/commands/help.ts
1043
- import color8 from "picocolors";
1044
- var help = {
1045
- name: "help",
1046
- description: "show help",
1047
- usage: "noto help [command]",
1048
- execute: async (options) => {
1049
- const command6 = getCommand(options._[0]);
1050
- if (command6 && command6.name !== "help") {
1051
- console.log();
1052
- console.log(color8.bold("usage"));
1053
- console.log(` ${command6.usage}`);
1054
- console.log();
1055
- console.log(color8.bold("description"));
1056
- console.log(` ${command6.description}`);
1057
- console.log();
1058
- } else {
1059
- const commands = listCommand();
1060
- console.log();
1061
- console.log(color8.bold("usage"));
1062
- console.log(` noto [command] [options]`);
1063
- console.log();
1064
- console.log(color8.bold("commands"));
1065
- commands.forEach((command7) => {
1066
- console.log(` ${color8.bold(command7.name)} ${color8.dim(command7.description)}`);
1067
- });
1068
- await exit(0);
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);
1069
1205
  }
1206
+ const suffix = msg ? `
1207
+ ${msg}` : "";
1208
+ spin.stop(color8.red(`failed to generate commit message${suffix}`), 1);
1209
+ await exit(1);
1070
1210
  }
1071
- };
1072
- var help_default = help;
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
+ }
1073
1222
 
1074
1223
  // src/commands/index.ts
1075
- var commands = [noto_default, prev_default, branch_default, checkout_default, config_default, help_default];
1076
- var getCommand = (name, cmds = commands) => {
1077
- return cmds.find((cmd) => cmd.name === name);
1224
+ var commands = {
1225
+ checkout: checkout2,
1226
+ config,
1227
+ prev,
1228
+ init,
1229
+ noto
1078
1230
  };
1079
- var listCommand = () => {
1080
- return commands;
1081
- };
1082
- // package.json
1083
- var version = "1.2.9";
1231
+
1232
+ // src/router.ts
1233
+ var router = t.router(commands);
1084
1234
 
1085
1235
  // src/index.ts
1086
- var globalSpec = {
1087
- "--version": Boolean,
1088
- "--help": Boolean,
1089
- "-v": "--version",
1090
- "-h": "--help"
1091
- };
1092
- function main() {
1093
- const args = process.argv.slice(2);
1094
- const { command: command6, options: globalOptions } = parse(globalSpec, args);
1095
- console.log();
1096
- p8.intro(`${color9.bgCyan(color9.black(" @snelusha/noto "))}`);
1097
- if (globalOptions["--version"])
1098
- return p8.outro(version);
1099
- if (globalOptions["--help"]) {
1100
- getCommand("help")?.execute(globalOptions);
1101
- return;
1102
- }
1103
- const cmd = getCommand(command6) ?? getCommand("noto");
1104
- if (!cmd)
1105
- return getCommand("noto")?.execute(globalOptions);
1106
- let commandArgs = args;
1107
- let selectedCommand = cmd;
1108
- if (cmd.subCommands && commandArgs.length) {
1109
- const possibleCommand = commandArgs[1];
1110
- const subCommand = cmd.subCommands.find((cmd2) => cmd2.name === possibleCommand || cmd2.aliases && cmd2.aliases.includes(possibleCommand));
1111
- if (subCommand) {
1112
- selectedCommand = subCommand;
1113
- commandArgs = commandArgs.slice(2);
1114
- }
1115
- }
1116
- const commandSpec = (selectedCommand.options ?? []).reduce((acc, opt) => {
1117
- acc[opt.flag] = opt.type ?? Boolean;
1118
- if (Array.isArray(opt.alias))
1119
- opt.alias.forEach((alias) => acc[alias] = opt.flag);
1120
- else if (opt.alias)
1121
- acc[opt.alias] = opt.flag;
1122
- return acc;
1123
- }, {});
1124
- const { options: commandOptions } = safeParse(commandSpec, commandArgs);
1125
- const options = { ...globalOptions, ...commandOptions };
1126
- selectedCommand.execute(options);
1127
- }
1128
- main();
1236
+ createCli({
1237
+ name: "noto",
1238
+ router,
1239
+ version
1240
+ }).run();