@lodestar-oss/quicksave 0.2.0 → 0.3.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/dist/index.mjs +396 -149
- package/package.json +8 -8
package/dist/index.mjs
CHANGED
|
@@ -1,22 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cli, command, description, name, option, parse, printErrors, printHelp, printVersion, schema, version } from "@bomb.sh/router";
|
|
3
|
-
import { cancel,
|
|
4
|
-
import {
|
|
3
|
+
import { cancel, intro, log, outro } from "@clack/prompts";
|
|
4
|
+
import { BaseError, DEFAULT_FAILURE_REASON, UnexpectedError, composeErrorMessage, isError } from "@little-nebulae/error";
|
|
5
|
+
import { nodeLoadEnvFileList } from "@little-nebulae/node-env";
|
|
6
|
+
import { prettifyError, z } from "zod";
|
|
5
7
|
import { NonEmptyStringSchema } from "@little-nebulae/zod-schemas";
|
|
6
|
-
import {
|
|
8
|
+
import { constants } from "node:os";
|
|
9
|
+
import { attemptAsync, fail, succeed } from "@little-nebulae/result";
|
|
10
|
+
import { x } from "tinyexec";
|
|
7
11
|
import { createGroq } from "@ai-sdk/groq";
|
|
8
12
|
import { generateText } from "ai";
|
|
9
|
-
import {
|
|
13
|
+
import { clackConfirm } from "@little-nebulae/clack";
|
|
14
|
+
// src/constants.ts
|
|
15
|
+
const CANCEL_EXIT_CODE = 128 + constants.signals.SIGINT;
|
|
16
|
+
const OPERATION_FAILED_OUTRO = "Operation failed.";
|
|
17
|
+
const MODE_ENUM = {
|
|
18
|
+
STAGED: "staged",
|
|
19
|
+
ALL: "all",
|
|
20
|
+
ASK: "ask"
|
|
21
|
+
};
|
|
22
|
+
const LOCKFILE_TUPLE = [
|
|
23
|
+
"package-lock.json",
|
|
24
|
+
"pnpm-lock.yaml",
|
|
25
|
+
"yarn.lock",
|
|
26
|
+
"deno.lock",
|
|
27
|
+
"bun.lock"
|
|
28
|
+
];
|
|
10
29
|
// package.json
|
|
11
|
-
var version$1 = "0.
|
|
30
|
+
var version$1 = "0.3.0";
|
|
12
31
|
// src/app.ts
|
|
13
32
|
const SwitchOptionSchema = z.preprocess((value) => typeof value === "undefined" ? false : value, z.boolean());
|
|
14
|
-
const app = command(name("quicksave"), description("A conventional commit generator using LLM."), version(version$1), option(name("cwd"), cli(["--cwd", "-C"]), schema(NonEmptyStringSchema.default(process.cwd()))), option(name(
|
|
33
|
+
const app = command(name("quicksave"), description("A conventional commit generator using LLM."), version(version$1), option(name("cwd"), cli(["--cwd", "-C"]), schema(NonEmptyStringSchema.default(process.cwd()))), option(name(MODE_ENUM.STAGED), cli(["--staged", "-s"], { switch: true }), schema(SwitchOptionSchema)), option(name(MODE_ENUM.ALL), cli(["--all", "-a"], { switch: true }), schema(SwitchOptionSchema)));
|
|
15
34
|
// src/config/env.ts
|
|
16
35
|
const EnvSchema = z.object({ GROQ_API_KEY: z.templateLiteral(["gsk_", NonEmptyStringSchema]) });
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
// src/features/git/ops/restore-index.ts
|
|
37
|
+
async function restoreIndex({ cwd, treeId }) {
|
|
38
|
+
return attemptAsync({
|
|
39
|
+
tryFn: async () => {
|
|
40
|
+
await x("git", ["read-tree", treeId], {
|
|
41
|
+
nodeOptions: { cwd },
|
|
42
|
+
throwOnError: true
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
catchFn: (error) => new UnexpectedError({
|
|
46
|
+
message: composeErrorMessage({ operation: "unstaged the added changes" }),
|
|
47
|
+
cause: error,
|
|
48
|
+
meta: null
|
|
49
|
+
})
|
|
50
|
+
});
|
|
19
51
|
}
|
|
52
|
+
// src/errors/empty-commit-message.ts
|
|
53
|
+
var EmptyCommitMessageError = class extends BaseError {
|
|
54
|
+
name = "EmptyCommitMessageError";
|
|
55
|
+
code = "EMPTY_COMMIT_MESSAGE_ERROR";
|
|
56
|
+
constructor(meta) {
|
|
57
|
+
super({
|
|
58
|
+
message: "The LLM provider responded with empty content",
|
|
59
|
+
cause: null,
|
|
60
|
+
meta
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
20
64
|
// src/features/ai/system-prompt.ts
|
|
21
65
|
const SYSTEM_PROMPT = `You write Git commit messages that follow the Conventional Commits 1.0.0 specification.
|
|
22
66
|
|
|
@@ -69,106 +113,281 @@ Footers
|
|
|
69
113
|
- Be specific. Prefer "fix(auth): reject expired refresh tokens" over "fix: update auth logic".
|
|
70
114
|
- Never mention that you are an AI or that the message was generated.`;
|
|
71
115
|
// src/features/ai/ops/generate-commit-message.ts
|
|
72
|
-
async function generateCommitMessage({ diff }) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
116
|
+
async function generateCommitMessage({ diff, env }) {
|
|
117
|
+
return attemptAsync({
|
|
118
|
+
tryFn: async () => {
|
|
119
|
+
const groqProvider = createGroq({ apiKey: env.GROQ_API_KEY });
|
|
120
|
+
const generation = await generateText({
|
|
121
|
+
model: groqProvider("openai/gpt-oss-20b"),
|
|
122
|
+
instructions: SYSTEM_PROMPT,
|
|
123
|
+
prompt: `<diff>\n${diff}\n</diff>`
|
|
124
|
+
});
|
|
125
|
+
const message = generation.text.trim();
|
|
126
|
+
const meta = {
|
|
127
|
+
finishReason: generation.finishReason,
|
|
128
|
+
usage: generation.usage,
|
|
129
|
+
warnings: generation.warnings,
|
|
130
|
+
finalStep: generation.finalStep
|
|
131
|
+
};
|
|
132
|
+
if (message.length === 0) throw new EmptyCommitMessageError(meta);
|
|
133
|
+
return {
|
|
134
|
+
message,
|
|
135
|
+
meta
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
catchFn: (error) => {
|
|
139
|
+
if (error instanceof EmptyCommitMessageError) return error;
|
|
140
|
+
return new UnexpectedError({
|
|
141
|
+
message: composeErrorMessage({ operation: "generate commit message" }),
|
|
142
|
+
cause: error,
|
|
143
|
+
meta: null
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// src/errors/empty-diff.ts
|
|
149
|
+
var EmptyDiffError = class extends BaseError {
|
|
150
|
+
name = "EmptyDiffError";
|
|
151
|
+
code = "EMPTY_DIFF_ERROR";
|
|
152
|
+
constructor() {
|
|
153
|
+
super({
|
|
154
|
+
message: "No staged changes to commit.",
|
|
155
|
+
cause: null,
|
|
156
|
+
meta: null
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
// src/features/git/ops/get-staged-diff.ts
|
|
161
|
+
async function getStagedDiff({ cwd }) {
|
|
162
|
+
return attemptAsync({
|
|
163
|
+
tryFn: async () => {
|
|
164
|
+
const diff = (await x("git", [
|
|
165
|
+
"diff",
|
|
166
|
+
"--staged",
|
|
167
|
+
"--no-color",
|
|
168
|
+
"--no-ext-diff",
|
|
169
|
+
"--find-renames",
|
|
170
|
+
"--",
|
|
171
|
+
":/",
|
|
172
|
+
...LOCKFILE_TUPLE.map((f) => `:(exclude,glob)**/${f}`)
|
|
173
|
+
], {
|
|
174
|
+
nodeOptions: { cwd },
|
|
175
|
+
throwOnError: true
|
|
176
|
+
})).stdout.trim();
|
|
177
|
+
if (diff.length === 0) throw new EmptyDiffError();
|
|
178
|
+
return diff;
|
|
179
|
+
},
|
|
180
|
+
catchFn: (error) => {
|
|
181
|
+
if (error instanceof EmptyDiffError) return error;
|
|
182
|
+
return new UnexpectedError({
|
|
183
|
+
message: composeErrorMessage({ operation: "get staged diff" }),
|
|
184
|
+
cause: error,
|
|
185
|
+
meta: null
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// src/ops/get-commit-message.ts
|
|
191
|
+
async function getCommitMessage({ cwd, env }) {
|
|
192
|
+
const getStagedDiffResult = await getStagedDiff({ cwd });
|
|
193
|
+
if (!getStagedDiffResult.success) return getStagedDiffResult;
|
|
194
|
+
const diff = getStagedDiffResult.data;
|
|
195
|
+
return await generateCommitMessage({
|
|
196
|
+
diff,
|
|
197
|
+
env
|
|
79
198
|
});
|
|
80
|
-
return text.trim();
|
|
81
199
|
}
|
|
82
200
|
// src/features/git/ops/commit-staged-changes.ts
|
|
83
201
|
async function commitStagedChanges({ cwd, message }) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
202
|
+
return attemptAsync({
|
|
203
|
+
tryFn: async () => {
|
|
204
|
+
await x("git", [
|
|
205
|
+
"commit",
|
|
206
|
+
"-m",
|
|
207
|
+
message
|
|
208
|
+
], {
|
|
209
|
+
throwOnError: true,
|
|
210
|
+
nodeOptions: { cwd }
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
catchFn: (error) => new UnexpectedError({
|
|
214
|
+
message: composeErrorMessage({ operation: "commit staged changes" }),
|
|
215
|
+
cause: error,
|
|
216
|
+
meta: null
|
|
217
|
+
})
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// src/ops/handle-commit.ts
|
|
221
|
+
async function handleCommit({ isTTY, cwd, message }) {
|
|
222
|
+
let shouldCommit = true;
|
|
223
|
+
if (isTTY) {
|
|
224
|
+
const confirmCommitResult = await clackConfirm({
|
|
225
|
+
message: "Do you want to commit with the generated message?",
|
|
226
|
+
isTTY
|
|
227
|
+
});
|
|
228
|
+
if (!confirmCommitResult.success) return confirmCommitResult;
|
|
229
|
+
shouldCommit = confirmCommitResult.data;
|
|
230
|
+
}
|
|
231
|
+
if (!shouldCommit) return succeed({ committed: false });
|
|
232
|
+
const commitStagedChangesResult = await commitStagedChanges({
|
|
233
|
+
cwd,
|
|
87
234
|
message
|
|
88
|
-
], {
|
|
89
|
-
throwOnError: true,
|
|
90
|
-
nodeOptions: { cwd }
|
|
91
235
|
});
|
|
236
|
+
if (!commitStagedChangesResult.success) return commitStagedChangesResult;
|
|
237
|
+
return succeed({ committed: true });
|
|
92
238
|
}
|
|
93
239
|
// src/features/git/ops/get-all-changes.ts
|
|
94
240
|
async function getAllChanges({ cwd }) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
241
|
+
return attemptAsync({
|
|
242
|
+
tryFn: async () => {
|
|
243
|
+
const { stdout } = await x("git", [
|
|
244
|
+
"status",
|
|
245
|
+
"--porcelain=v1",
|
|
246
|
+
"-z",
|
|
247
|
+
"--untracked-files=all"
|
|
248
|
+
], {
|
|
249
|
+
nodeOptions: { cwd },
|
|
250
|
+
throwOnError: true
|
|
251
|
+
});
|
|
252
|
+
const changes = [];
|
|
253
|
+
if (stdout.trim().length === 0) return changes;
|
|
254
|
+
const parts = stdout.split("\0").filter((part) => part.length > 0);
|
|
255
|
+
for (let i = 0; i < parts.length; i++) {
|
|
256
|
+
const entry = parts[i];
|
|
257
|
+
const staged = entry[0];
|
|
258
|
+
const unstaged = entry[1];
|
|
259
|
+
const path = entry.slice(3);
|
|
260
|
+
if (staged === "R" || staged === "C") i++;
|
|
261
|
+
changes.push({
|
|
262
|
+
path,
|
|
263
|
+
status: {
|
|
264
|
+
staged,
|
|
265
|
+
unstaged
|
|
266
|
+
}
|
|
267
|
+
});
|
|
118
268
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"pnpm-lock.yaml",
|
|
127
|
-
"yarn.lock",
|
|
128
|
-
"deno.lock",
|
|
129
|
-
"bun.lock"
|
|
130
|
-
];
|
|
131
|
-
// src/features/git/ops/get-staged-diff.ts
|
|
132
|
-
async function getStagedDiff({ cwd }) {
|
|
133
|
-
return (await x("git", [
|
|
134
|
-
"diff",
|
|
135
|
-
"--staged",
|
|
136
|
-
"--no-color",
|
|
137
|
-
"--no-ext-diff",
|
|
138
|
-
"--find-renames",
|
|
139
|
-
"--",
|
|
140
|
-
":/",
|
|
141
|
-
...LOCKFILE_TUPLE.map((f) => `:(exclude,glob)**/${f}`)
|
|
142
|
-
], {
|
|
143
|
-
nodeOptions: { cwd },
|
|
144
|
-
throwOnError: true
|
|
145
|
-
})).stdout.trim();
|
|
146
|
-
}
|
|
147
|
-
// src/features/git/ops/restore-index.ts
|
|
148
|
-
async function restoreIndex({ cwd, treeId }) {
|
|
149
|
-
await x("git", ["read-tree", treeId], {
|
|
150
|
-
nodeOptions: { cwd },
|
|
151
|
-
throwOnError: true
|
|
269
|
+
return changes;
|
|
270
|
+
},
|
|
271
|
+
catchFn: (error) => new UnexpectedError({
|
|
272
|
+
message: composeErrorMessage({ operation: "get all changes made to the repo" }),
|
|
273
|
+
cause: error,
|
|
274
|
+
meta: null
|
|
275
|
+
})
|
|
152
276
|
});
|
|
153
277
|
}
|
|
154
278
|
// src/features/git/ops/snapshot-index.ts
|
|
155
279
|
async function snapshotIndex({ cwd }) {
|
|
156
|
-
return (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
280
|
+
return attemptAsync({
|
|
281
|
+
tryFn: async () => {
|
|
282
|
+
return (await x("git", ["write-tree"], {
|
|
283
|
+
nodeOptions: { cwd },
|
|
284
|
+
throwOnError: true
|
|
285
|
+
})).stdout.trim();
|
|
286
|
+
},
|
|
287
|
+
catchFn: (error) => new UnexpectedError({
|
|
288
|
+
message: composeErrorMessage({ operation: "snapshot the current state of the repo" }),
|
|
289
|
+
cause: error,
|
|
290
|
+
meta: null
|
|
291
|
+
})
|
|
292
|
+
});
|
|
160
293
|
}
|
|
161
294
|
// src/features/git/ops/stage-all-changes.ts
|
|
162
295
|
async function stageAllChanges({ cwd }) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
296
|
+
return attemptAsync({
|
|
297
|
+
tryFn: async () => {
|
|
298
|
+
await x("git", ["add", "-A"], {
|
|
299
|
+
nodeOptions: { cwd },
|
|
300
|
+
throwOnError: true
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
catchFn: (error) => new UnexpectedError({
|
|
304
|
+
message: composeErrorMessage({ operation: "add all unstaged changes" }),
|
|
305
|
+
cause: error,
|
|
306
|
+
meta: null
|
|
307
|
+
})
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
// src/ops/handle-unstaged-changes.ts
|
|
311
|
+
async function handleUnstagedChanges({ cwd, mode }) {
|
|
312
|
+
if (mode === "staged") return succeed({
|
|
313
|
+
snapshotted: false,
|
|
314
|
+
reason: "In staged mode, unstaged changes are ignored."
|
|
315
|
+
});
|
|
316
|
+
const getAllChangesResult = await getAllChanges({ cwd });
|
|
317
|
+
if (!getAllChangesResult.success) return getAllChangesResult;
|
|
318
|
+
if (!getAllChangesResult.data.some((change) => change.status.unstaged !== " ")) return succeed({
|
|
319
|
+
snapshotted: false,
|
|
320
|
+
reason: "The repo has no unstaged changes"
|
|
321
|
+
});
|
|
322
|
+
if (mode === "ask") {
|
|
323
|
+
const confirmStageResult = await clackConfirm({
|
|
324
|
+
message: "You have unstaged changes. Do you want to add them all?",
|
|
325
|
+
isTTY: true
|
|
326
|
+
});
|
|
327
|
+
if (!confirmStageResult.success) return confirmStageResult;
|
|
328
|
+
if (!confirmStageResult.data) return succeed({
|
|
329
|
+
snapshotted: false,
|
|
330
|
+
reason: "The user doesn't want to add all unstaged changes"
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const snapshotIndexResult = await snapshotIndex({ cwd });
|
|
334
|
+
if (!snapshotIndexResult.success) return snapshotIndexResult;
|
|
335
|
+
const treeId = snapshotIndexResult.data;
|
|
336
|
+
const stageAllChangesResult = await stageAllChanges({ cwd });
|
|
337
|
+
if (!stageAllChangesResult.success) return stageAllChangesResult;
|
|
338
|
+
return succeed({
|
|
339
|
+
snapshotted: true,
|
|
340
|
+
treeId
|
|
166
341
|
});
|
|
167
342
|
}
|
|
343
|
+
// src/errors/conflicting-options.ts
|
|
344
|
+
var ConflictingOptionsError = class extends BaseError {
|
|
345
|
+
name = "ConflictingOptionsError";
|
|
346
|
+
code = "CONFLICTING_OPTIONS_ERROR";
|
|
347
|
+
constructor({ message, options }) {
|
|
348
|
+
super({
|
|
349
|
+
message,
|
|
350
|
+
cause: options,
|
|
351
|
+
meta: null
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
// src/errors/missing-options.ts
|
|
356
|
+
var MissingOptionsError = class extends BaseError {
|
|
357
|
+
name = "MissingOptionsError";
|
|
358
|
+
code = "MISSING_OPTIONS_ERROR";
|
|
359
|
+
constructor({ message, options }) {
|
|
360
|
+
super({
|
|
361
|
+
message,
|
|
362
|
+
cause: options,
|
|
363
|
+
meta: null
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
// src/ops/resolve-mode.ts
|
|
368
|
+
function resolveMode({ staged, all, isTTY }) {
|
|
369
|
+
if (staged && all) return fail(new ConflictingOptionsError({
|
|
370
|
+
message: `Conflicting options: ${MODE_ENUM.STAGED}, ${MODE_ENUM.ALL}`,
|
|
371
|
+
options: {
|
|
372
|
+
staged,
|
|
373
|
+
all
|
|
374
|
+
}
|
|
375
|
+
}));
|
|
376
|
+
if (!isTTY && !staged && !all) return fail(new MissingOptionsError({
|
|
377
|
+
message: "The staged or all option is required in a non-interactive terminal context.",
|
|
378
|
+
options: {
|
|
379
|
+
staged,
|
|
380
|
+
all
|
|
381
|
+
}
|
|
382
|
+
}));
|
|
383
|
+
if (staged) return succeed("staged");
|
|
384
|
+
if (all) return succeed("all");
|
|
385
|
+
return succeed("ask");
|
|
386
|
+
}
|
|
168
387
|
// src/index.ts
|
|
169
388
|
const result = parse(app, { argv: process.argv.slice(2) });
|
|
170
389
|
if (!result.ok) {
|
|
171
|
-
|
|
390
|
+
console.error(printErrors(result));
|
|
172
391
|
process.exit(1);
|
|
173
392
|
}
|
|
174
393
|
switch (result.method) {
|
|
@@ -180,89 +399,117 @@ switch (result.method) {
|
|
|
180
399
|
break;
|
|
181
400
|
case "execute": switch (result.route) {
|
|
182
401
|
case "/": {
|
|
402
|
+
intro("Initiating quicksave...");
|
|
183
403
|
const { cwd, staged, all } = result.model;
|
|
184
404
|
const isTTY = process.stdin.isTTY;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
405
|
+
const resolveModeResult = resolveMode({
|
|
406
|
+
staged,
|
|
407
|
+
all,
|
|
408
|
+
isTTY
|
|
409
|
+
});
|
|
410
|
+
if (!resolveModeResult.success) {
|
|
411
|
+
log.error(resolveModeResult.error.message);
|
|
412
|
+
outro(OPERATION_FAILED_OUTRO);
|
|
191
413
|
process.exit(1);
|
|
192
414
|
}
|
|
193
|
-
|
|
415
|
+
const mode = resolveModeResult.data;
|
|
416
|
+
nodeLoadEnvFileList({
|
|
194
417
|
parentDirPath: cwd,
|
|
195
|
-
|
|
418
|
+
envFileList: [".env.local", ".env"]
|
|
196
419
|
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
420
|
+
const parseEnvResult = EnvSchema.safeParse(process.env);
|
|
421
|
+
if (!parseEnvResult.success) {
|
|
422
|
+
log.error("Invalid GROQ_API_KEY environment variable:");
|
|
423
|
+
log.message(prettifyError(parseEnvResult.error));
|
|
424
|
+
outro("Please fix the above issue to continue.");
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
const env = parseEnvResult.data;
|
|
428
|
+
const handleUnstagedChangesResult = await handleUnstagedChanges({
|
|
429
|
+
cwd,
|
|
430
|
+
mode
|
|
200
431
|
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const shouldStage = await confirm({ message: "You have unstaged changes. Do you want to add them all?" });
|
|
207
|
-
if (isCancel(shouldStage)) {
|
|
208
|
-
cancel("Operation cancelled.");
|
|
209
|
-
process.exit(0);
|
|
210
|
-
}
|
|
211
|
-
willStage = shouldStage;
|
|
212
|
-
}
|
|
213
|
-
if (willStage) {
|
|
214
|
-
treeId = await snapshotIndex({ cwd });
|
|
215
|
-
await stageAllChanges({ cwd });
|
|
216
|
-
}
|
|
432
|
+
if (!handleUnstagedChangesResult.success) {
|
|
433
|
+
const handleUnstagedChangesError = handleUnstagedChangesResult.error;
|
|
434
|
+
if (handleUnstagedChangesError.code === "CLACK_IS_CANCELLED_ERROR") {
|
|
435
|
+
cancel(handleUnstagedChangesError.message);
|
|
436
|
+
process.exit(CANCEL_EXIT_CODE);
|
|
217
437
|
}
|
|
438
|
+
log.error(handleUnstagedChangesError.message);
|
|
439
|
+
log.message(isError(handleUnstagedChangesError.cause) ? handleUnstagedChangesError.cause.message : DEFAULT_FAILURE_REASON);
|
|
440
|
+
outro(OPERATION_FAILED_OUTRO);
|
|
441
|
+
process.exit(1);
|
|
218
442
|
}
|
|
219
|
-
const
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
443
|
+
const snapshotStatus = handleUnstagedChangesResult.data;
|
|
444
|
+
if (!snapshotStatus.snapshotted && snapshotStatus.reason === "The repo has no unstaged changes") log.warn(snapshotStatus.reason);
|
|
445
|
+
log.info("Generating commit message...");
|
|
446
|
+
const getCommitMessageResult = await getCommitMessage({
|
|
447
|
+
cwd,
|
|
448
|
+
env
|
|
449
|
+
});
|
|
450
|
+
if (!getCommitMessageResult.success) {
|
|
451
|
+
const getCommitMessageError = getCommitMessageResult.error;
|
|
452
|
+
log.error(getCommitMessageError.message);
|
|
453
|
+
if (getCommitMessageError.code === "UNEXPECTED_ERROR") log.message(isError(getCommitMessageError.cause) ? getCommitMessageError.cause.message : DEFAULT_FAILURE_REASON);
|
|
454
|
+
if (snapshotStatus.snapshotted) {
|
|
455
|
+
const restoreIndexResult = await restoreIndex({
|
|
224
456
|
cwd,
|
|
225
|
-
treeId
|
|
457
|
+
treeId: snapshotStatus.treeId
|
|
226
458
|
});
|
|
227
|
-
|
|
459
|
+
if (!restoreIndexResult.success) {
|
|
460
|
+
const restoreIndexError = restoreIndexResult.error;
|
|
461
|
+
log.error(restoreIndexError.message);
|
|
462
|
+
log.message(isError(restoreIndexError.cause) ? restoreIndexError.cause.message : DEFAULT_FAILURE_REASON);
|
|
463
|
+
}
|
|
228
464
|
}
|
|
229
|
-
outro(
|
|
230
|
-
process.exit(
|
|
465
|
+
outro(OPERATION_FAILED_OUTRO);
|
|
466
|
+
process.exit(1);
|
|
231
467
|
}
|
|
232
|
-
log.info("Generating commit message...");
|
|
233
|
-
const message = await generateCommitMessage({ diff });
|
|
234
468
|
log.success("Commit message generated:");
|
|
469
|
+
const { message } = getCommitMessageResult.data;
|
|
235
470
|
log.message(message);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
471
|
+
const handleCommitResult = await handleCommit({
|
|
472
|
+
isTTY,
|
|
473
|
+
cwd,
|
|
474
|
+
message
|
|
475
|
+
});
|
|
476
|
+
if (!handleCommitResult.success) {
|
|
477
|
+
const handleCommitError = handleCommitResult.error;
|
|
478
|
+
if (handleCommitError.code === "CLACK_IS_CANCELLED_ERROR") cancel(handleCommitError.message);
|
|
479
|
+
else {
|
|
480
|
+
log.error(handleCommitError.message);
|
|
481
|
+
log.message(isError(handleCommitError.cause) ? handleCommitError.cause.message : DEFAULT_FAILURE_REASON);
|
|
482
|
+
}
|
|
483
|
+
if (snapshotStatus.snapshotted) {
|
|
484
|
+
const restoreIndexResult = await restoreIndex({
|
|
485
|
+
cwd,
|
|
486
|
+
treeId: snapshotStatus.treeId
|
|
487
|
+
});
|
|
488
|
+
if (!restoreIndexResult.success) {
|
|
489
|
+
const restoreIndexError = restoreIndexResult.error;
|
|
490
|
+
log.error(restoreIndexError.message);
|
|
491
|
+
log.message(isError(restoreIndexError.cause) ? restoreIndexError.cause.message : DEFAULT_FAILURE_REASON);
|
|
245
492
|
}
|
|
246
|
-
cancel("Operation cancelled.");
|
|
247
|
-
process.exit(0);
|
|
248
493
|
}
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
494
|
+
if (handleCommitError.code === "CLACK_IS_CANCELLED_ERROR") process.exit(CANCEL_EXIT_CODE);
|
|
495
|
+
outro(OPERATION_FAILED_OUTRO);
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
const { committed } = handleCommitResult.data;
|
|
499
|
+
if (!committed) {
|
|
500
|
+
if (snapshotStatus.snapshotted) {
|
|
501
|
+
const restoreIndexResult = await restoreIndex({
|
|
502
|
+
cwd,
|
|
503
|
+
treeId: snapshotStatus.treeId
|
|
504
|
+
});
|
|
505
|
+
if (!restoreIndexResult.success) {
|
|
506
|
+
const restoreIndexError = restoreIndexResult.error;
|
|
507
|
+
log.error(restoreIndexError.message);
|
|
508
|
+
log.message(isError(restoreIndexError.cause) ? restoreIndexError.cause.message : DEFAULT_FAILURE_REASON);
|
|
256
509
|
}
|
|
257
|
-
outro("Nothing was committed.");
|
|
258
|
-
process.exit(0);
|
|
259
510
|
}
|
|
260
511
|
}
|
|
261
|
-
|
|
262
|
-
cwd,
|
|
263
|
-
message
|
|
264
|
-
});
|
|
265
|
-
outro("Changes committed.");
|
|
512
|
+
outro(committed ? "Changes committed." : "Nothing was commited.");
|
|
266
513
|
process.exit(0);
|
|
267
514
|
}
|
|
268
515
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lodestar-oss/quicksave",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"qs": "./dist/index.mjs",
|
|
@@ -15,21 +15,21 @@
|
|
|
15
15
|
"tag": "latest"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@ai-sdk/groq": "4.0.
|
|
18
|
+
"@ai-sdk/groq": "4.0.72",
|
|
19
19
|
"@bomb.sh/router": "0.8.0",
|
|
20
20
|
"@clack/prompts": "1.8.1",
|
|
21
|
-
"@little-nebulae/
|
|
22
|
-
"@little-nebulae/
|
|
21
|
+
"@little-nebulae/clack": "0.4.0",
|
|
22
|
+
"@little-nebulae/error": "1.4.1",
|
|
23
|
+
"@little-nebulae/node-env": "0.5.0",
|
|
23
24
|
"@little-nebulae/result": "1.4.0",
|
|
24
|
-
"@little-nebulae/system-error": "2.1.0",
|
|
25
25
|
"@little-nebulae/zod-schemas": "0.2.0",
|
|
26
|
-
"ai": "7.0.
|
|
26
|
+
"ai": "7.0.156",
|
|
27
27
|
"tinyexec": "1.3.1",
|
|
28
28
|
"zod": "4.6.7"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@oxc-node/cli": "1.0.1",
|
|
32
|
-
"@types/node": "26.6.
|
|
32
|
+
"@types/node": "26.6.6",
|
|
33
33
|
"oxfmt": "0.75.0",
|
|
34
34
|
"oxlint": "1.90.0",
|
|
35
35
|
"oxlint-tsgolint": "7.0.2005",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"devEngines": {
|
|
43
43
|
"packageManager": {
|
|
44
44
|
"name": "pnpm",
|
|
45
|
-
"version": "12.
|
|
45
|
+
"version": "12.16.1",
|
|
46
46
|
"onFail": "warn"
|
|
47
47
|
}
|
|
48
48
|
},
|