@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.
Files changed (2) hide show
  1. package/dist/index.mjs +396 -149
  2. 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, confirm, isCancel, log, outro } from "@clack/prompts";
4
- import { ENV_FILE_NAME, loadDotEnvFile } from "@little-nebulae/node-env";
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 { z } from "zod";
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 { x } from "tinyexec";
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.2.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("staged"), cli(["--staged", "-s"], { switch: true }), schema(SwitchOptionSchema)), option(name("all"), cli(["--all", "-a"], { switch: true }), schema(SwitchOptionSchema)));
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
- function getEnvVars() {
18
- return EnvSchema.parse(process.env);
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
- const env = getEnvVars();
74
- const groqProvider = createGroq({ apiKey: env.GROQ_API_KEY });
75
- const { text } = await generateText({
76
- model: groqProvider("openai/gpt-oss-20b"),
77
- instructions: SYSTEM_PROMPT,
78
- prompt: `<diff>\n${diff}\n</diff>`
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
- await x("git", [
85
- "commit",
86
- "-m",
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
- const { stdout } = await x("git", [
96
- "status",
97
- "--porcelain=v1",
98
- "-z",
99
- "--untracked-files=all"
100
- ], {
101
- nodeOptions: { cwd },
102
- throwOnError: true
103
- });
104
- const changes = [];
105
- if (stdout.trim().length === 0) return changes;
106
- const parts = stdout.split("\0").filter((part) => part.length > 0);
107
- for (let i = 0; i < parts.length; i++) {
108
- const entry = parts[i];
109
- const staged = entry[0];
110
- const unstaged = entry[1];
111
- const path = entry.slice(3);
112
- if (staged === "R" || staged === "C") i++;
113
- changes.push({
114
- path,
115
- status: {
116
- staged,
117
- unstaged
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
- return changes;
122
- }
123
- // src/constants.ts
124
- const LOCKFILE_TUPLE = [
125
- "package-lock.json",
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 (await x("git", ["write-tree"], {
157
- nodeOptions: { cwd },
158
- throwOnError: true
159
- })).stdout.trim();
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
- await x("git", ["add", "-A"], {
164
- nodeOptions: { cwd },
165
- throwOnError: true
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
- log.error(printErrors(result));
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
- if (staged && all) {
186
- log.error("Conflicting options: staged, all");
187
- process.exit(1);
188
- }
189
- if (!isTTY && !staged && !all) {
190
- log.error("The staged or all flag is required in non-interactive terminals.");
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
- loadDotEnvFile({
415
+ const mode = resolveModeResult.data;
416
+ nodeLoadEnvFileList({
194
417
  parentDirPath: cwd,
195
- envFileName: ENV_FILE_NAME.LOCAL
418
+ envFileList: [".env.local", ".env"]
196
419
  });
197
- loadDotEnvFile({
198
- parentDirPath: cwd,
199
- envFileName: ENV_FILE_NAME.DEFAULT
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
- let treeId = void 0;
202
- if (!staged) {
203
- if ((await getAllChanges({ cwd })).some((change) => change.status.unstaged !== " ")) {
204
- let willStage = true;
205
- if (isTTY && !all) {
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 diff = await getStagedDiff({ cwd });
220
- if (diff.length === 0) {
221
- log.warn("No staged changes to commit.");
222
- if (treeId) {
223
- await restoreIndex({
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
- log.info("Unstaged the changes quicksave added.");
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("Exit early.");
230
- process.exit(0);
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
- if (isTTY) {
237
- const shouldCommit = await confirm({ message: "Do you want to commit with the above message?" });
238
- if (isCancel(shouldCommit)) {
239
- if (treeId) {
240
- await restoreIndex({
241
- cwd,
242
- treeId
243
- });
244
- log.info("Unstaged the changes quicksave added.");
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 (!shouldCommit) {
250
- if (treeId) {
251
- await restoreIndex({
252
- cwd,
253
- treeId
254
- });
255
- log.info("Unstaged the changes quicksave added.");
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
- await commitStagedChanges({
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.2.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.71",
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/error": "1.4.0",
22
- "@little-nebulae/node-env": "0.4.0",
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.154",
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.5",
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.15.0",
45
+ "version": "12.16.1",
46
46
  "onFail": "warn"
47
47
  }
48
48
  },