eckra 1.0.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 (43) hide show
  1. package/CONTRIBUTING.md +85 -0
  2. package/LICENSE +21 -0
  3. package/README.md +109 -0
  4. package/package.json +47 -0
  5. package/screenshot.jpg +0 -0
  6. package/src/helpers/ai.js +240 -0
  7. package/src/helpers/config.js +122 -0
  8. package/src/helpers/git.js +655 -0
  9. package/src/helpers/lmstudio.js +11 -0
  10. package/src/helpers/patch.js +91 -0
  11. package/src/index.js +73 -0
  12. package/src/ui/app.js +177 -0
  13. package/src/ui/branch.js +295 -0
  14. package/src/ui/commit.js +250 -0
  15. package/src/ui/common.js +106 -0
  16. package/src/ui/config.js +269 -0
  17. package/src/ui/log.js +146 -0
  18. package/src/ui/menu.js +393 -0
  19. package/src/ui/modules/amend.js +43 -0
  20. package/src/ui/modules/blame.js +56 -0
  21. package/src/ui/modules/branch.js +223 -0
  22. package/src/ui/modules/commit.js +232 -0
  23. package/src/ui/modules/conflict.js +93 -0
  24. package/src/ui/modules/diff.js +68 -0
  25. package/src/ui/modules/log.js +52 -0
  26. package/src/ui/modules/more.js +94 -0
  27. package/src/ui/modules/rebase.js +72 -0
  28. package/src/ui/modules/remote.js +74 -0
  29. package/src/ui/modules/search.js +46 -0
  30. package/src/ui/modules/settings.js +123 -0
  31. package/src/ui/modules/stage.js +174 -0
  32. package/src/ui/modules/stash.js +96 -0
  33. package/src/ui/modules/stats.js +57 -0
  34. package/src/ui/modules/status.js +86 -0
  35. package/src/ui/modules/sync.js +73 -0
  36. package/src/ui/modules/tag.js +85 -0
  37. package/src/ui/modules/undo.js +49 -0
  38. package/src/ui/modules/worktree.js +131 -0
  39. package/src/ui/push.js +184 -0
  40. package/src/ui/status.js +156 -0
  41. package/tests/ai.test.js +112 -0
  42. package/tests/config.test.js +123 -0
  43. package/tests/patch.test.js +44 -0
@@ -0,0 +1,94 @@
1
+ const inquirer = require("inquirer");
2
+ const { s, header, clear } = require("../common");
3
+ const { doUndo } = require("./undo");
4
+ const { doAmend } = require("./amend");
5
+ const { doDiff } = require("./diff");
6
+ const { doStash } = require("./stash");
7
+ const { doTag } = require("./tag");
8
+ const { doRemote } = require("./remote");
9
+ const { doStats } = require("./stats");
10
+ const { doSearch } = require("./search");
11
+ const { doBlame } = require("./blame");
12
+ const { doWorktree } = require("./worktree");
13
+ const { doSettings } = require("./settings");
14
+ const { doRebase } = require("./rebase");
15
+
16
+ async function doMore() {
17
+ clear();
18
+ header();
19
+ console.log(s.bold(" More Options\n"));
20
+
21
+ const { action } = await inquirer.prompt([
22
+ {
23
+ type: "list",
24
+ name: "action",
25
+ message: s.muted("What would you like to do?"),
26
+ choices: [
27
+ { name: s.warning(" ↩ Undo (Revert last commit)"), value: "undo" },
28
+ {
29
+ name: s.primary(" ✎ Amend (Fix commit message)"),
30
+ value: "amend",
31
+ },
32
+ { name: s.warning(" ⚡ Rebase / Squash"), value: "rebase" },
33
+ { name: s.text(" ≋ Diff (View changes)"), value: "diff" },
34
+ { type: "separator", line: " " },
35
+ { name: s.text(" 📦 Stash"), value: "stash" },
36
+ { name: s.text(" 🏷 Tag"), value: "tag" },
37
+ { name: s.text(" 🔗 Remote"), value: "remote" },
38
+ { type: "separator", line: " " },
39
+ { name: s.text(" 📊 Statistics"), value: "stats" },
40
+ { name: s.text(" 🔍 Search Commits"), value: "search" },
41
+ { name: s.text(" 📋 Blame"), value: "blame" },
42
+ { name: s.text(" 🌳 Worktrees"), value: "worktree" },
43
+ { type: "separator", line: " " },
44
+ { name: s.text(" ⚙ Settings"), value: "settings" },
45
+ { name: s.muted(" ← Main Menu"), value: "back" },
46
+ ],
47
+ pageSize: 15,
48
+ loop: false,
49
+ },
50
+ ]);
51
+
52
+ if (action === "back") return;
53
+
54
+ switch (action) {
55
+ case "undo":
56
+ await doUndo();
57
+ break;
58
+ case "amend":
59
+ await doAmend();
60
+ break;
61
+ case "rebase":
62
+ await doRebase();
63
+ break;
64
+ case "diff":
65
+ await doDiff();
66
+ break;
67
+ case "stash":
68
+ await doStash();
69
+ break;
70
+ case "tag":
71
+ await doTag();
72
+ break;
73
+ case "remote":
74
+ await doRemote();
75
+ break;
76
+ case "stats":
77
+ await doStats();
78
+ break;
79
+ case "search":
80
+ await doSearch();
81
+ break;
82
+ case "blame":
83
+ await doBlame();
84
+ break;
85
+ case "worktree":
86
+ await doWorktree();
87
+ break;
88
+ case "settings":
89
+ await doSettings();
90
+ break;
91
+ }
92
+ }
93
+
94
+ module.exports = { doMore };
@@ -0,0 +1,72 @@
1
+ const inquirer = require("inquirer");
2
+ const ora = require("ora");
3
+ const { getCommitLog, squashCommits, resetToCommit } = require("../../helpers/git");
4
+ const { s, header, clear, pause } = require("../common");
5
+
6
+ async function doRebase() {
7
+ clear();
8
+ header();
9
+ console.log(s.bold(" Advanced Git Operations (Rebase)\n"));
10
+
11
+ const { action } = await inquirer.prompt([
12
+ {
13
+ type: "list",
14
+ name: "action",
15
+ message: s.muted("Select operation:"),
16
+ choices: [
17
+ { name: s.warning(" Squash last N commits"), value: "squash" },
18
+ { name: s.muted(" ← Back"), value: "back" },
19
+ ],
20
+ loop: false,
21
+ },
22
+ ]);
23
+
24
+ if (action === "back") return;
25
+
26
+ if (action === "squash") {
27
+ await doSquash();
28
+ }
29
+ }
30
+
31
+ async function doSquash() {
32
+ const log = await getCommitLog(10);
33
+
34
+ if (log.all.length < 2) {
35
+ console.log(s.warning(" Not enough commits to squash."));
36
+ await pause();
37
+ return;
38
+ }
39
+
40
+ const { count } = await inquirer.prompt([
41
+ {
42
+ type: "number",
43
+ name: "count",
44
+ message: "How many commits to squash (from HEAD)?",
45
+ default: 2,
46
+ validate: (val) => val > 1 && val <= log.all.length ? true : `Enter a number between 2 and ${log.all.length}`
47
+ }
48
+ ]);
49
+
50
+ const { message } = await inquirer.prompt([
51
+ {
52
+ type: "input",
53
+ name: "message",
54
+ message: "New commit message for squashed commit:",
55
+ default: `Squashed ${count} commits`
56
+ }
57
+ ]);
58
+
59
+ const spin = ora({ text: s.muted(" Squashing..."), spinner: "dots" }).start();
60
+
61
+ try {
62
+ // Soft reset to HEAD~count
63
+ await squashCommits(count, message);
64
+ spin.succeed(s.success(" Commits squashed successfully!"));
65
+ } catch (error) {
66
+ spin.fail(s.error(" Squash failed: " + error.message));
67
+ }
68
+
69
+ await pause();
70
+ }
71
+
72
+ module.exports = { doRebase };
@@ -0,0 +1,74 @@
1
+ const inquirer = require("inquirer");
2
+ const { getRemotes, addRemote, removeRemote } = require("../../helpers/git");
3
+ const { s, header, clear, pause, sleep } = require("../common");
4
+
5
+ async function doRemote() {
6
+ clear();
7
+ header();
8
+ console.log(s.bold(" Remote\n"));
9
+
10
+ const remotes = await getRemotes();
11
+
12
+ if (remotes.length > 0) {
13
+ remotes.forEach((r) => {
14
+ console.log(s.primary(` ${r.name}`));
15
+ console.log(s.muted(` ${r.refs.fetch || "-"}\n`));
16
+ });
17
+ } else {
18
+ console.log(s.muted(" No remotes.\n"));
19
+ }
20
+
21
+ const { action } = await inquirer.prompt([
22
+ {
23
+ type: "list",
24
+ name: "action",
25
+ message: s.muted("What should I do?"),
26
+ choices: [
27
+ { name: s.success(" + Add Remote"), value: "add" },
28
+ { name: s.error(" ✕ Remove Remote"), value: "remove" },
29
+ { name: s.muted(" ← Back"), value: "back" },
30
+ ],
31
+ loop: false,
32
+ },
33
+ ]);
34
+
35
+ if (action === "back") return;
36
+
37
+ if (action === "add") {
38
+ const { name } = await inquirer.prompt([
39
+ {
40
+ type: "input",
41
+ name: "name",
42
+ message: s.muted("Remote name:"),
43
+ default: "origin",
44
+ },
45
+ ]);
46
+ const { url } = await inquirer.prompt([
47
+ {
48
+ type: "input",
49
+ name: "url",
50
+ message: s.muted("URL:"),
51
+ validate: (v) => v.length > 0,
52
+ },
53
+ ]);
54
+ await addRemote(name, url);
55
+ console.log(s.success(`\n ✓ ${name} added!`));
56
+ await sleep(600);
57
+ }
58
+
59
+ if (action === "remove" && remotes.length > 0) {
60
+ const { toRemove } = await inquirer.prompt([
61
+ {
62
+ type: "list",
63
+ name: "toRemove",
64
+ message: s.muted("Which remote to remove?"),
65
+ choices: remotes.map((r) => r.name),
66
+ },
67
+ ]);
68
+ await removeRemote(toRemove);
69
+ console.log(s.success(`\n ✓ ${toRemove} removed!`));
70
+ await sleep(600);
71
+ }
72
+ }
73
+
74
+ module.exports = { doRemote };
@@ -0,0 +1,46 @@
1
+ const inquirer = require("inquirer");
2
+ const ora = require("ora");
3
+ const { searchCommits } = require("../../helpers/git");
4
+ const { s, header, clear, pause, truncate, timeAgo } = require("../common");
5
+
6
+ async function doSearch() {
7
+ clear();
8
+ header();
9
+ console.log(s.bold(" Search Commits\n"));
10
+
11
+ const { query } = await inquirer.prompt([
12
+ {
13
+ type: "input",
14
+ name: "query",
15
+ message: s.muted("Search term:"),
16
+ validate: (v) => v.length > 0,
17
+ },
18
+ ]);
19
+
20
+ const spin = ora({ text: s.muted(" Searching..."), spinner: "dots" }).start();
21
+
22
+ try {
23
+ const results = await searchCommits(query);
24
+ spin.stop();
25
+
26
+ if (results.all.length === 0) {
27
+ console.log(s.muted("\n No results found.\n"));
28
+ } else {
29
+ console.log(s.muted(`\n ${results.all.length} results:\n`));
30
+ results.all.slice(0, 10).forEach((commit) => {
31
+ console.log(
32
+ ` ${s.primary(commit.hash.substring(0, 7))} ${s.text(truncate(commit.message, 50))}`,
33
+ );
34
+ console.log(
35
+ s.muted(` ${commit.author_name} · ${timeAgo(commit.date)}\n`),
36
+ );
37
+ });
38
+ }
39
+ } catch (err) {
40
+ spin.fail(s.error(` ${err.message}`));
41
+ }
42
+
43
+ await pause();
44
+ }
45
+
46
+ module.exports = { doSearch };
@@ -0,0 +1,123 @@
1
+ const inquirer = require("inquirer");
2
+ const { getConfig, saveConfig } = require("../../helpers/config");
3
+ const { checkAIConnection } = require("../../helpers/ai");
4
+ const { s, header, clear, sleep, truncate } = require("../common");
5
+
6
+ async function doSettings() {
7
+ clear();
8
+ header();
9
+ console.log(s.bold(" Settings\n"));
10
+
11
+ const config = getConfig();
12
+ const aiStatus = await checkAIConnection();
13
+
14
+ console.log(s.muted(" Provider: ") + s.text(config.aiProvider || "lmstudio"));
15
+
16
+ if (config.aiProvider === "openai") {
17
+ console.log(s.muted(" Model: ") + s.text(config.openaiModel));
18
+ console.log(s.muted(" API Key: ") + s.text(config.openaiApiKey ? "****" + config.openaiApiKey.slice(-4) : "None"));
19
+ } else if (config.aiProvider === "anthropic") {
20
+ console.log(s.muted(" Model: ") + s.text(config.anthropicModel));
21
+ console.log(s.muted(" API Key: ") + s.text(config.anthropicApiKey ? "****" + config.anthropicApiKey.slice(-4) : "None"));
22
+ } else if (config.aiProvider === "ollama") {
23
+ console.log(s.muted(" URL: ") + s.text(config.ollamaUrl));
24
+ console.log(s.muted(" Model: ") + s.text(config.ollamaModel));
25
+ } else {
26
+ console.log(s.muted(" LM Studio URL: ") + s.text(config.lmStudioUrl));
27
+ console.log(s.muted(" Model: ") + s.text(config.model));
28
+ }
29
+
30
+ console.log(s.muted(" AI Instruction: ") + s.text(truncate(config.aiInstruction || "", 50)));
31
+ console.log(
32
+ s.muted(" AI Status: ") +
33
+ (aiStatus.connected ? s.success("Connected ✓") : s.error("Not connected ✗ (" + (aiStatus.error || "Unknown error") + ")")),
34
+ );
35
+ console.log();
36
+
37
+ const { action } = await inquirer.prompt([
38
+ {
39
+ type: "list",
40
+ name: "action",
41
+ message: s.muted("What should I do?"),
42
+ choices: [
43
+ { name: s.text(" Change Provider"), value: "provider" },
44
+ { name: s.text(" Configure Provider Settings"), value: "configure" },
45
+ { name: s.text(" Change AI Instructions"), value: "instruction" },
46
+ { name: s.muted(" ← Back"), value: "back" },
47
+ ],
48
+ loop: false,
49
+ },
50
+ ]);
51
+
52
+ if (action === "back") return;
53
+
54
+ if (action === "provider") {
55
+ const { provider } = await inquirer.prompt([
56
+ {
57
+ type: "list",
58
+ name: "provider",
59
+ message: s.muted("Select AI Provider:"),
60
+ choices: [
61
+ { name: "LM Studio (Local)", value: "lmstudio" },
62
+ { name: "OpenAI", value: "openai" },
63
+ { name: "Anthropic (Claude)", value: "anthropic" },
64
+ { name: "Ollama (Local)", value: "ollama" },
65
+ ],
66
+ default: config.aiProvider,
67
+ },
68
+ ]);
69
+ saveConfig({ ...config, aiProvider: provider });
70
+ console.log(s.success("\n ✓ Provider changed to " + provider));
71
+ await sleep(600);
72
+ return;
73
+ }
74
+
75
+ if (action === "configure") {
76
+ const provider = config.aiProvider || "lmstudio";
77
+ let questions = [];
78
+
79
+ if (provider === "openai") {
80
+ questions = [
81
+ { type: "input", name: "openaiApiKey", message: "OpenAI API Key:", default: config.openaiApiKey },
82
+ { type: "input", name: "openaiModel", message: "Model (e.g. gpt-4o, gpt-3.5-turbo):", default: config.openaiModel }
83
+ ];
84
+ } else if (provider === "anthropic") {
85
+ questions = [
86
+ { type: "input", name: "anthropicApiKey", message: "Anthropic API Key:", default: config.anthropicApiKey },
87
+ { type: "input", name: "anthropicModel", message: "Model (e.g. claude-3-5-sonnet-20240620):", default: config.anthropicModel }
88
+ ];
89
+ } else if (provider === "ollama") {
90
+ questions = [
91
+ { type: "input", name: "ollamaUrl", message: "Ollama URL:", default: config.ollamaUrl },
92
+ { type: "input", name: "ollamaModel", message: "Model (e.g. llama3):", default: config.ollamaModel }
93
+ ];
94
+ } else {
95
+ questions = [
96
+ { type: "input", name: "lmStudioUrl", message: "LM Studio URL:", default: config.lmStudioUrl },
97
+ { type: "input", name: "model", message: "Model:", default: config.model }
98
+ ];
99
+ }
100
+
101
+ const answers = await inquirer.prompt(questions);
102
+ saveConfig({ ...config, ...answers });
103
+ console.log(s.success("\n ✓ Settings saved!"));
104
+ await sleep(600);
105
+ return;
106
+ }
107
+
108
+ if (action === "instruction") {
109
+ const { instruction } = await inquirer.prompt([
110
+ {
111
+ type: "input",
112
+ name: "instruction",
113
+ message: s.muted("AI System Instruction:"),
114
+ default: config.aiInstruction,
115
+ },
116
+ ]);
117
+ saveConfig({ ...config, aiInstruction: instruction });
118
+ console.log(s.success("\n ✓ Saved!"));
119
+ await sleep(600);
120
+ }
121
+ }
122
+
123
+ module.exports = { doSettings };
@@ -0,0 +1,174 @@
1
+ const inquirer = require("inquirer");
2
+ const ora = require("ora");
3
+ const { getGitStatus, stageAll, stageFiles, getFileDiff, applyPatchString } = require("../../helpers/git");
4
+ const { parseDiff, generatePatch } = require("../../helpers/patch");
5
+ const { s, header, clear, sleep, rows, pause } = require("../common");
6
+ const { doCommit } = require("./commit");
7
+
8
+ async function doStage(info) {
9
+ clear();
10
+ header();
11
+ console.log(s.bold(" Stage\n"));
12
+
13
+ const status = info?.status || (await getGitStatus());
14
+ const files = [...status.modified, ...status.not_added];
15
+
16
+ if (files.length === 0) {
17
+ console.log(s.muted(" No changes.\n"));
18
+ await pause();
19
+ return;
20
+ }
21
+
22
+ // Categorize files
23
+ const modifiedFiles = status.modified.map((f) => ({
24
+ name: ` ${s.warning("~")} ${f}`,
25
+ value: f,
26
+ short: f,
27
+ }));
28
+
29
+ const untrackedFiles = status.not_added.map((f) => ({
30
+ name: ` ${s.muted("+")} ${f}`,
31
+ value: f,
32
+ short: f,
33
+ }));
34
+
35
+ const { action } = await inquirer.prompt([
36
+ {
37
+ type: "list",
38
+ name: "action",
39
+ message: s.muted("What should I do?"),
40
+ choices: [
41
+ { name: s.success(" ✓ Stage All"), value: "all" },
42
+ { name: s.text(" ◉ Select Files"), value: "select" },
43
+ { name: s.warning(" ✂ Partial Stage (Beta)"), value: "partial" },
44
+ { name: s.muted(" ← Back"), value: "back" },
45
+ ],
46
+ loop: false,
47
+ },
48
+ ]);
49
+
50
+ if (action === "back") return;
51
+ if (action === "partial") return doPartialStage(status);
52
+
53
+ if (action === "all") {
54
+ const spin = ora({ text: s.muted(" Staging..."), spinner: "dots" }).start();
55
+ await stageAll();
56
+ spin.succeed(s.success(" All staged!"));
57
+ await sleep(500);
58
+
59
+ // Redirect to commit
60
+ const { goCommit } = await inquirer.prompt([
61
+ {
62
+ type: "confirm",
63
+ name: "goCommit",
64
+ message: s.muted("Would you like to commit?"),
65
+ default: true,
66
+ },
67
+ ]);
68
+
69
+ if (goCommit) await doCommit();
70
+ return;
71
+ }
72
+
73
+ // Select files
74
+ const { selected } = await inquirer.prompt([
75
+ {
76
+ type: "checkbox",
77
+ name: "selected",
78
+ message: s.muted("Select files (use space):"),
79
+ choices: [
80
+ ...(modifiedFiles.length > 0
81
+ ? [{ type: "separator", line: s.muted(" Modified") }]
82
+ : []),
83
+ ...modifiedFiles,
84
+ ...(untrackedFiles.length > 0
85
+ ? [{ type: "separator", line: s.muted(" Untracked") }]
86
+ : []),
87
+ ...untrackedFiles,
88
+ ],
89
+ pageSize: rows() - 10,
90
+ loop: false,
91
+ },
92
+ ]);
93
+
94
+ if (selected.length > 0) {
95
+ await stageFiles(selected);
96
+ console.log(s.success(`\n ✓ ${selected.length} files staged!`));
97
+
98
+ const { goCommit } = await inquirer.prompt([
99
+ {
100
+ type: "confirm",
101
+ name: "goCommit",
102
+ message: s.muted("Would you like to commit?"),
103
+ default: true,
104
+ },
105
+ ]);
106
+
107
+ if (goCommit) await doCommit();
108
+ }
109
+ }
110
+
111
+ async function doPartialStage(status) {
112
+ if (status.modified.length === 0) {
113
+ console.log(s.warning("\n No modified files suitable for partial staging."));
114
+ console.log(s.muted(" (Untracked files cannot be partially staged)"));
115
+ await pause();
116
+ return;
117
+ }
118
+
119
+ // Select a file
120
+ const { file } = await inquirer.prompt([
121
+ {
122
+ type: "list",
123
+ name: "file",
124
+ message: s.muted("Select file to split:"),
125
+ choices: status.modified.map(f => ({ name: f, value: f })),
126
+ },
127
+ ]);
128
+
129
+ const diff = await getFileDiff(file);
130
+ const parsedFiles = parseDiff(diff);
131
+
132
+ if (parsedFiles.length === 0 || !parsedFiles[0].hunks.length) {
133
+ console.log(s.warning(" No hunks found to split."));
134
+ await pause();
135
+ return;
136
+ }
137
+
138
+ const targetFile = parsedFiles[0];
139
+ const hunks = targetFile.hunks;
140
+
141
+ const { selectedIndices } = await inquirer.prompt([
142
+ {
143
+ type: "checkbox",
144
+ name: "selectedIndices",
145
+ message: s.muted("Select hunks to stage:"),
146
+ choices: hunks.map((hunk, idx) => {
147
+ // Create a preview of the hunk
148
+ const preview = hunk.lines.slice(0, 4).join("\n ");
149
+ const more = hunk.lines.length > 4 ? `... (+${hunk.lines.length - 4} lines)` : "";
150
+ return {
151
+ name: `${s.primary(`Hunk ${idx + 1}`)}\n ${s.dim(preview)} ${s.dim(more)}`,
152
+ value: idx
153
+ };
154
+ }),
155
+ pageSize: rows() - 5
156
+ }
157
+ ]);
158
+
159
+ if (selectedIndices.length === 0) return;
160
+
161
+ const spin = ora({ text: s.muted(" Applying partial patch..."), spinner: "dots" }).start();
162
+
163
+ try {
164
+ const patchContent = generatePatch(targetFile, selectedIndices);
165
+ await applyPatchString(patchContent);
166
+ spin.succeed(s.success(" Selected hunks staged!"));
167
+ } catch (error) {
168
+ spin.fail(s.error(" Failed to stage hunks: " + error.message));
169
+ }
170
+
171
+ await sleep(1000);
172
+ }
173
+
174
+ module.exports = { doStage };
@@ -0,0 +1,96 @@
1
+ const inquirer = require("inquirer");
2
+ const { getGitStatus, listStashes, stashChanges, popStash, applyStash, dropStash } = require("../../helpers/git");
3
+ const { s, header, clear, pause, sleep } = require("../common");
4
+
5
+ async function doStash() {
6
+ clear();
7
+ header();
8
+ console.log(s.bold(" Stash\n"));
9
+
10
+ const stashes = await listStashes();
11
+
12
+ if (stashes.all.length > 0) {
13
+ stashes.all.slice(0, 5).forEach((st, i) => {
14
+ console.log(s.muted(` ${i}: `) + s.text(st.message));
15
+ });
16
+ console.log();
17
+ } else {
18
+ console.log(s.muted(" No stashes.\n"));
19
+ }
20
+
21
+ const { action } = await inquirer.prompt([
22
+ {
23
+ type: "list",
24
+ name: "action",
25
+ message: s.muted("What should I do?"),
26
+ choices: [
27
+ { name: s.success(" + Save Stash"), value: "save" },
28
+ { name: s.primary(" ↓ Pop Stash (Apply & Drop)"), value: "pop" },
29
+ { name: s.text(" ⟳ Apply Stash (Keep)"), value: "apply" },
30
+ { name: s.error(" ✕ Drop Stash"), value: "drop" },
31
+ { name: s.muted(" ← Back"), value: "back" },
32
+ ],
33
+ loop: false,
34
+ },
35
+ ]);
36
+
37
+ if (action === "back") return;
38
+
39
+ if (action === "save") {
40
+ const status = await getGitStatus();
41
+ if (status.modified.length === 0 && status.not_added.length === 0) {
42
+ console.log(s.muted("\n No changes to stash."));
43
+ await pause();
44
+ } else {
45
+ const { message } = await inquirer.prompt([
46
+ {
47
+ type: "input",
48
+ name: "message",
49
+ message: s.muted("Stash message (optional):"),
50
+ },
51
+ ]);
52
+ await stashChanges(message || null);
53
+ console.log(s.success("\n ✓ Changes stashed!"));
54
+ await sleep(600);
55
+ }
56
+ return;
57
+ }
58
+
59
+ if (stashes.all.length === 0) {
60
+ console.log(s.muted("\n No stashes available."));
61
+ await pause();
62
+ return;
63
+ }
64
+
65
+ // Select stash index
66
+ const { index } = await inquirer.prompt([
67
+ {
68
+ type: "list",
69
+ name: "index",
70
+ message: s.muted("Select stash:"),
71
+ choices: stashes.all.map((st, i) => ({
72
+ name: `${i}: ${st.message}`,
73
+ value: i
74
+ }))
75
+ }
76
+ ]);
77
+
78
+ try {
79
+ if (action === "pop") {
80
+ await popStash(index);
81
+ console.log(s.success("\n ✓ Stash popped!"));
82
+ } else if (action === "apply") {
83
+ await applyStash(index);
84
+ console.log(s.success("\n ✓ Stash applied!"));
85
+ } else if (action === "drop") {
86
+ await dropStash(index);
87
+ console.log(s.success("\n ✓ Stash dropped!"));
88
+ }
89
+ await sleep(600);
90
+ } catch (err) {
91
+ console.log(s.error(`\n ✗ ${err.message}`));
92
+ await pause();
93
+ }
94
+ }
95
+
96
+ module.exports = { doStash };