gittable 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.
- package/LICENSE +22 -0
- package/README.md +459 -0
- package/cli.js +342 -0
- package/commands/add.js +159 -0
- package/commands/blame.js +33 -0
- package/commands/branch.js +234 -0
- package/commands/checkout.js +43 -0
- package/commands/cherry-pick.js +104 -0
- package/commands/clean.js +71 -0
- package/commands/clone.js +76 -0
- package/commands/commit.js +82 -0
- package/commands/config.js +171 -0
- package/commands/diff.js +30 -0
- package/commands/fetch.js +76 -0
- package/commands/grep.js +42 -0
- package/commands/init.js +45 -0
- package/commands/log.js +38 -0
- package/commands/merge.js +69 -0
- package/commands/mv.js +40 -0
- package/commands/pull.js +74 -0
- package/commands/push.js +97 -0
- package/commands/rebase.js +134 -0
- package/commands/remote.js +236 -0
- package/commands/restore.js +76 -0
- package/commands/revert.js +63 -0
- package/commands/rm.js +57 -0
- package/commands/show.js +47 -0
- package/commands/stash.js +201 -0
- package/commands/status.js +21 -0
- package/commands/sync.js +98 -0
- package/commands/tag.js +153 -0
- package/commands/undo.js +200 -0
- package/commands/uninit.js +57 -0
- package/index.d.ts +56 -0
- package/index.js +55 -0
- package/lib/commit/build-commit.js +64 -0
- package/lib/commit/get-previous-commit.js +15 -0
- package/lib/commit/questions.js +226 -0
- package/lib/config/read-config-file.js +54 -0
- package/lib/git/exec.js +222 -0
- package/lib/ui/ascii.js +154 -0
- package/lib/ui/banner.js +80 -0
- package/lib/ui/status-display.js +90 -0
- package/lib/ui/table.js +76 -0
- package/lib/utils/email-prompt.js +62 -0
- package/lib/utils/logger.js +47 -0
- package/lib/utils/spinner.js +57 -0
- package/lib/utils/terminal-link.js +55 -0
- package/lib/versions.js +17 -0
- package/package.json +73 -0
- package/standalone.js +24 -0
package/lib/git/exec.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
const { execSync } = require('node:child_process');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Execute a git command and return the output
|
|
5
|
+
*/
|
|
6
|
+
const execGit = (command, options = {}) => {
|
|
7
|
+
const { silent = false, encoding = 'utf8' } = options;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const result = execSync(`git ${command}`, {
|
|
11
|
+
encoding,
|
|
12
|
+
stdio: silent ? ['pipe', 'pipe', 'pipe'] : 'inherit',
|
|
13
|
+
...options,
|
|
14
|
+
});
|
|
15
|
+
return { success: true, output: result, error: null };
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const stdout = error.stdout?.toString() || '';
|
|
18
|
+
const stderr = error.stderr?.toString() || '';
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
output: stdout,
|
|
22
|
+
error: stderr || error.message,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if we're in a git repository
|
|
29
|
+
*/
|
|
30
|
+
const isGitRepo = () => {
|
|
31
|
+
const result = execGit('rev-parse --git-dir', { silent: true });
|
|
32
|
+
return result.success;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get current branch name
|
|
37
|
+
*/
|
|
38
|
+
const getCurrentBranch = () => {
|
|
39
|
+
const result = execGit('rev-parse --abbrev-ref HEAD', { silent: true });
|
|
40
|
+
return result.success ? result.output.trim() : null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get repository status
|
|
45
|
+
*/
|
|
46
|
+
const getStatus = () => {
|
|
47
|
+
const result = execGit('status --porcelain', { silent: true });
|
|
48
|
+
if (!result.success) return null;
|
|
49
|
+
|
|
50
|
+
const lines = result.output.trim().split('\n').filter(Boolean);
|
|
51
|
+
const status = {
|
|
52
|
+
staged: [],
|
|
53
|
+
unstaged: [],
|
|
54
|
+
untracked: [],
|
|
55
|
+
ahead: 0,
|
|
56
|
+
behind: 0,
|
|
57
|
+
diverged: false,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const staged = line[0];
|
|
62
|
+
const unstaged = line[1];
|
|
63
|
+
const file = line.slice(3);
|
|
64
|
+
|
|
65
|
+
if (staged !== ' ' && staged !== '?') {
|
|
66
|
+
status.staged.push({ status: staged, file });
|
|
67
|
+
}
|
|
68
|
+
if (unstaged !== ' ' && unstaged !== '?') {
|
|
69
|
+
if (unstaged === '?') {
|
|
70
|
+
status.untracked.push(file);
|
|
71
|
+
} else {
|
|
72
|
+
status.unstaged.push({ status: unstaged, file });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check ahead/behind
|
|
78
|
+
const branchResult = execGit('rev-list --left-right --count HEAD...@{u}', { silent: true });
|
|
79
|
+
if (branchResult.success) {
|
|
80
|
+
const [behind, ahead] = branchResult.output.trim().split('\t').map(Number);
|
|
81
|
+
status.ahead = ahead || 0;
|
|
82
|
+
status.behind = behind || 0;
|
|
83
|
+
status.diverged = ahead > 0 && behind > 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return status;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get list of branches
|
|
91
|
+
*/
|
|
92
|
+
const getBranches = () => {
|
|
93
|
+
const localResult = execGit('branch -vv', { silent: true });
|
|
94
|
+
const remoteResult = execGit('branch -r', { silent: true });
|
|
95
|
+
|
|
96
|
+
const branches = {
|
|
97
|
+
local: [],
|
|
98
|
+
remote: [],
|
|
99
|
+
current: null,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (localResult.success) {
|
|
103
|
+
const localLines = localResult.output.trim().split('\n').filter(Boolean);
|
|
104
|
+
for (const line of localLines) {
|
|
105
|
+
const isCurrent = line.startsWith('*');
|
|
106
|
+
const trimmed = line.replace(/^\*\s*/, '').trim();
|
|
107
|
+
|
|
108
|
+
// Parse: branch-name [upstream: ahead/behind] or branch-name
|
|
109
|
+
const match = trimmed.match(/^(\S+)(?:\s+\[([^\]]+)\])?/);
|
|
110
|
+
if (match) {
|
|
111
|
+
const name = match[1];
|
|
112
|
+
const upstreamInfo = match[2] || '';
|
|
113
|
+
|
|
114
|
+
// Extract upstream branch name (before colon or space)
|
|
115
|
+
const upstreamMatch = upstreamInfo.match(/^([^:]+)/);
|
|
116
|
+
const upstream = upstreamMatch ? upstreamMatch[1].trim() : null;
|
|
117
|
+
|
|
118
|
+
if (isCurrent) {
|
|
119
|
+
branches.current = name;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
branches.local.push({
|
|
123
|
+
name,
|
|
124
|
+
current: isCurrent,
|
|
125
|
+
upstream: upstream,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (remoteResult.success) {
|
|
132
|
+
const remoteLines = remoteResult.output.trim().split('\n').filter(Boolean);
|
|
133
|
+
for (const line of remoteLines) {
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
// Remove remote prefix (e.g., "origin/")
|
|
136
|
+
const match = trimmed.match(/^(\S+?)\/(.+)$/);
|
|
137
|
+
if (match) {
|
|
138
|
+
branches.remote.push({
|
|
139
|
+
name: match[2],
|
|
140
|
+
remote: match[1],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return branches;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get commit log
|
|
151
|
+
*/
|
|
152
|
+
const getLog = (limit = 20, format = '%h|%an|%ar|%s') => {
|
|
153
|
+
const result = execGit(`log --format="${format}" -n ${limit}`, { silent: true });
|
|
154
|
+
if (!result.success) return [];
|
|
155
|
+
|
|
156
|
+
return result.output
|
|
157
|
+
.trim()
|
|
158
|
+
.split('\n')
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.map((line) => {
|
|
161
|
+
const [hash, author, date, ...messageParts] = line.split('|');
|
|
162
|
+
return {
|
|
163
|
+
hash,
|
|
164
|
+
author,
|
|
165
|
+
date,
|
|
166
|
+
message: messageParts.join('|'),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get stash list
|
|
173
|
+
*/
|
|
174
|
+
const getStashList = () => {
|
|
175
|
+
const result = execGit('stash list --format="%gd|%ar|%gs"', { silent: true });
|
|
176
|
+
if (!result.success) return [];
|
|
177
|
+
|
|
178
|
+
return result.output
|
|
179
|
+
.trim()
|
|
180
|
+
.split('\n')
|
|
181
|
+
.filter(Boolean)
|
|
182
|
+
.map((line) => {
|
|
183
|
+
const parts = line.split('|');
|
|
184
|
+
const ref = parts[0] || '';
|
|
185
|
+
const date = parts[1] || '';
|
|
186
|
+
const message = parts.slice(2).join('|') || '';
|
|
187
|
+
return {
|
|
188
|
+
ref,
|
|
189
|
+
date,
|
|
190
|
+
message,
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check if a remote exists
|
|
197
|
+
*/
|
|
198
|
+
const remoteExists = (remoteName = 'origin') => {
|
|
199
|
+
const result = execGit(`remote get-url ${remoteName}`, { silent: true });
|
|
200
|
+
return result.success;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get list of remotes
|
|
205
|
+
*/
|
|
206
|
+
const getRemotes = () => {
|
|
207
|
+
const result = execGit('remote', { silent: true });
|
|
208
|
+
if (!result.success) return [];
|
|
209
|
+
return result.output.trim().split('\n').filter(Boolean);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
execGit,
|
|
214
|
+
isGitRepo,
|
|
215
|
+
getCurrentBranch,
|
|
216
|
+
getStatus,
|
|
217
|
+
getBranches,
|
|
218
|
+
getLog,
|
|
219
|
+
getStashList,
|
|
220
|
+
remoteExists,
|
|
221
|
+
getRemotes,
|
|
222
|
+
};
|
package/lib/ui/ascii.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
// ASCII art letter definitions from ASCII ART Reference.md
|
|
4
|
+
const ASCII_LETTERS = {
|
|
5
|
+
A: [
|
|
6
|
+
' ______ ',
|
|
7
|
+
'/\\ __ \\ ',
|
|
8
|
+
'\\ \\ __ \\ ',
|
|
9
|
+
' \\ \\_\\ \\_\\ ',
|
|
10
|
+
' \\/_/\\/_/ ',
|
|
11
|
+
],
|
|
12
|
+
B: [' ______ ', '/\\ == \\ ', '\\ \\ __< ', ' \\ \\_____\\ ', ' \\/_____/ '],
|
|
13
|
+
C: [' ______ ', '/\\ ___\\ ', '\\ \\ \\____ ', ' \\ \\_____\\ ', ' \\/_____/ '],
|
|
14
|
+
D: [' _____ ', '/\\ __-. ', '\\ \\ \\/\\ \\ ', ' \\ \\____- ', ' \\/____/ '],
|
|
15
|
+
E: [' ______ ', '/\\ ___\\ ', '\\ \\ __\\ ', ' \\ \\_____\\ ', ' \\/_____/ '],
|
|
16
|
+
F: [' ______ ', '/\\ ___\\ ', '\\ \\ __\\ ', ' \\ \\_\\ ', ' \\/_/ '],
|
|
17
|
+
G: [
|
|
18
|
+
' ______ ',
|
|
19
|
+
'/\\ ___\\ ',
|
|
20
|
+
'\\ \\ \\__ \\ ',
|
|
21
|
+
' \\ \\_____\\ ',
|
|
22
|
+
' \\/_____/ ',
|
|
23
|
+
],
|
|
24
|
+
H: [
|
|
25
|
+
' __ __ ',
|
|
26
|
+
'/\\ \\_\\ \\ ',
|
|
27
|
+
'\\ \\ __ \\ ',
|
|
28
|
+
' \\ \\_\\ \\_\\ ',
|
|
29
|
+
' \\/_/\\/_/ ',
|
|
30
|
+
],
|
|
31
|
+
I: [' __ ', '/\\ \\ ', '\\ \\ \\ ', ' \\ \\_\\ ', ' \\/_/ '],
|
|
32
|
+
J: [' __ ', ' /\\ \\ ', ' _\\_\\ \\ ', '/\\_____\\ ', '\\/_____/ '],
|
|
33
|
+
K: [
|
|
34
|
+
' __ __ ',
|
|
35
|
+
'/\\ \\/ / ',
|
|
36
|
+
'\\ \\ _"-. ',
|
|
37
|
+
' \\ \\_\\ \\_\\ ',
|
|
38
|
+
' \\/_/\\/_/ ',
|
|
39
|
+
],
|
|
40
|
+
L: [' __ ', '/\\ \\ ', '\\ \\ \\____ ', ' \\ \\_____\\ ', ' \\/_____/ '],
|
|
41
|
+
M: [
|
|
42
|
+
' __ __ ',
|
|
43
|
+
'/\\ "-./ \\ ',
|
|
44
|
+
'\\ \\ \\-./\\ \\ ',
|
|
45
|
+
' \\ \\_\\ \\ \\_\\ ',
|
|
46
|
+
' \\/_/ \\/_/ ',
|
|
47
|
+
],
|
|
48
|
+
N: [
|
|
49
|
+
' __ __ ',
|
|
50
|
+
'/\\ "-.\\ \\ ',
|
|
51
|
+
'\\ \\ \\-. \\ ',
|
|
52
|
+
' \\ \\_\\\\"\\_\\ ',
|
|
53
|
+
' \\/_/ \\/_/ ',
|
|
54
|
+
],
|
|
55
|
+
O: [' ______ ', '/\\ __ \\ ', '\\ \\ \\/\\ \\ ', ' \\ \\_____\\ ', ' \\/_____/ '],
|
|
56
|
+
P: [' ______ ', '/\\ == \\ ', '\\ \\ _-/ ', ' \\ \\_\\ ', ' \\/_/ '],
|
|
57
|
+
Q: [
|
|
58
|
+
' ______ ',
|
|
59
|
+
'/\\ __ \\ ',
|
|
60
|
+
'\\ \\ \\/\\_\\ ',
|
|
61
|
+
' \\ \\___\\_\\ ',
|
|
62
|
+
' \\/___/_/ ',
|
|
63
|
+
],
|
|
64
|
+
R: [
|
|
65
|
+
' ______ ',
|
|
66
|
+
'/\\ == \\ ',
|
|
67
|
+
'\\ \\ __< ',
|
|
68
|
+
' \\ \\_\\ \\_\\ ',
|
|
69
|
+
' \\/_/ /_/ ',
|
|
70
|
+
],
|
|
71
|
+
S: [' ______ ', '/\\ ___\\ ', '\\ \\___ \\ ', ' \\/\\_____\\ ', ' \\/_____/ '],
|
|
72
|
+
T: [' ______ ', '/\\__ _\\ ', '\\/_/\\ \\/ ', ' \\ \\_\\ ', ' \\/_/ '],
|
|
73
|
+
U: [
|
|
74
|
+
' __ __ ',
|
|
75
|
+
'/\\ \\/\\ \\ ',
|
|
76
|
+
'\\ \\ \\_\\ \\ ',
|
|
77
|
+
' \\ \\_____\\ ',
|
|
78
|
+
' \\/_____/ ',
|
|
79
|
+
],
|
|
80
|
+
V: [' __ __ ', '/\\ \\ / / ', "\\ \\ \\'/ ", ' \\ \\__| ', ' \\/_/ '],
|
|
81
|
+
W: [
|
|
82
|
+
' __ __ ',
|
|
83
|
+
'/\\ \\ _ \\ \\ ',
|
|
84
|
+
'\\ \\ \\/ ".\\ \\ ',
|
|
85
|
+
' \\ \\__/".~\\_\\',
|
|
86
|
+
' \\/_/ \\/_/',
|
|
87
|
+
],
|
|
88
|
+
X: [
|
|
89
|
+
' __ __ ',
|
|
90
|
+
'/\\_\\_\\_\\ ',
|
|
91
|
+
'\\/_/\\_\\/_ ',
|
|
92
|
+
' /\\_\\/\\_\\ ',
|
|
93
|
+
' \\/_/\\/_/ ',
|
|
94
|
+
],
|
|
95
|
+
Y: [
|
|
96
|
+
' __ __ ',
|
|
97
|
+
'/\\ \\_\\ \\ ',
|
|
98
|
+
'\\ \\____ \\ ',
|
|
99
|
+
' \\/\\_____\\ ',
|
|
100
|
+
' \\/_____/ ',
|
|
101
|
+
],
|
|
102
|
+
Z: [' ______ ', '/\\___ \\ ', '\\/_/ /__ ', ' /\\_____\\ ', ' \\/_____/ '],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate ASCII art for a word
|
|
107
|
+
* @param {string} word - The word to convert to ASCII art
|
|
108
|
+
* @param {object} options - Options for formatting
|
|
109
|
+
* @param {string} options.color - Chalk color to apply (default: 'cyan')
|
|
110
|
+
* @returns {string} - Formatted ASCII art
|
|
111
|
+
*/
|
|
112
|
+
function generateASCII(word, options = {}) {
|
|
113
|
+
const { color = 'cyan' } = options;
|
|
114
|
+
const upperWord = word.toUpperCase();
|
|
115
|
+
const letters = upperWord.split('');
|
|
116
|
+
|
|
117
|
+
// Filter out non-letter characters (spaces, numbers, etc.)
|
|
118
|
+
const validLetters = letters.filter((char) => ASCII_LETTERS[char]);
|
|
119
|
+
|
|
120
|
+
if (validLetters.length === 0) {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Get the height of ASCII art (all letters have the same height)
|
|
125
|
+
const height = ASCII_LETTERS[validLetters[0]].length;
|
|
126
|
+
|
|
127
|
+
// Combine letters horizontally
|
|
128
|
+
const lines = [];
|
|
129
|
+
for (let i = 0; i < height; i++) {
|
|
130
|
+
const line = validLetters.map((letter) => ASCII_LETTERS[letter][i]).join('');
|
|
131
|
+
lines.push(line);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Apply color if specified
|
|
135
|
+
const coloredLines = color ? lines.map((line) => chalk[color](line)) : lines;
|
|
136
|
+
|
|
137
|
+
return coloredLines.join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get ASCII art for a command name
|
|
142
|
+
* @param {string} commandName - The command name
|
|
143
|
+
* @param {object} options - Options for formatting
|
|
144
|
+
* @returns {string} - Formatted ASCII art
|
|
145
|
+
*/
|
|
146
|
+
function getCommandASCII(commandName, options = {}) {
|
|
147
|
+
return generateASCII(commandName, options);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
generateASCII,
|
|
152
|
+
getCommandASCII,
|
|
153
|
+
ASCII_LETTERS,
|
|
154
|
+
};
|
package/lib/ui/banner.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { getCommandASCII } = require('./ascii');
|
|
3
|
+
const commandVersions = require('../versions');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a banner for a command with ASCII art, borders, and version
|
|
7
|
+
* @param {string} commandName - The command name (e.g., 'COMMIT', 'STATUS')
|
|
8
|
+
* @param {object} options - Banner options
|
|
9
|
+
* @param {string} options.color - Color for ASCII art (default: 'cyan')
|
|
10
|
+
* @param {string} options.version - Version override (default: from versions.js)
|
|
11
|
+
* @param {string} options.borderColor - Border color (default: 'gray')
|
|
12
|
+
* @param {string} options.contentColor - Content color (default: 'cyan')
|
|
13
|
+
* @returns {string} - Formatted banner
|
|
14
|
+
*/
|
|
15
|
+
function createBanner(commandName, options = {}) {
|
|
16
|
+
const {
|
|
17
|
+
version = commandVersions[commandName.toLowerCase()] || '1.0.0',
|
|
18
|
+
borderColor = 'gray',
|
|
19
|
+
contentColor = 'cyan',
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
// Get ASCII art for the command
|
|
23
|
+
const asciiArt = getCommandASCII(commandName, { color: contentColor });
|
|
24
|
+
const asciiLines = asciiArt.split('\n').filter(Boolean);
|
|
25
|
+
|
|
26
|
+
if (asciiLines.length === 0) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Calculate width (use the longest line)
|
|
31
|
+
const WIDTH = Math.max(...asciiLines.map((line) => line.length));
|
|
32
|
+
|
|
33
|
+
const border = chalk[borderColor];
|
|
34
|
+
const content = chalk[contentColor];
|
|
35
|
+
|
|
36
|
+
// Helper functions
|
|
37
|
+
// Each framed line is: │ + space + content (WIDTH) + space + │ = WIDTH + 4 total
|
|
38
|
+
const _TOTAL_WIDTH = WIDTH + 4;
|
|
39
|
+
const pad = (text = '') => text.padEnd(WIDTH - 7);
|
|
40
|
+
const framedLine = (text = '') => `${border('│')} ${content(pad(text))} ${border('│')}`;
|
|
41
|
+
const topBorder = (title) => {
|
|
42
|
+
// Top border: ┌ + space + title + spaces + dashes + ╮ = TOTAL_WIDTH
|
|
43
|
+
// ┌ (1) + space (1) + titleText (titleLen) + dashes (dashCount) + ╮ (1) = WIDTH + 4
|
|
44
|
+
// So: 3 + titleLen + dashCount = WIDTH + 4
|
|
45
|
+
// Therefore: dashCount = WIDTH + 1 - titleLen
|
|
46
|
+
const titleText = `${title} `;
|
|
47
|
+
const titleLen = titleText.length;
|
|
48
|
+
const dashCount = WIDTH - 7 - titleLen;
|
|
49
|
+
return border(`${titleText}${'─'.repeat(dashCount)}╮`);
|
|
50
|
+
};
|
|
51
|
+
const bottomBorder = () => border(`├${'─'.repeat(WIDTH - 5)}╯`);
|
|
52
|
+
|
|
53
|
+
// Build banner
|
|
54
|
+
const banner = [
|
|
55
|
+
topBorder(`${commandName} v${version}`),
|
|
56
|
+
...asciiLines.map((line) => framedLine(` ${line}`)),
|
|
57
|
+
framedLine(),
|
|
58
|
+
bottomBorder(),
|
|
59
|
+
].join('\n');
|
|
60
|
+
|
|
61
|
+
return banner;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Display banner using clack.intro
|
|
66
|
+
* @param {string} commandName - The command name
|
|
67
|
+
* @param {object} options - Banner options
|
|
68
|
+
*/
|
|
69
|
+
function showBanner(commandName, options = {}) {
|
|
70
|
+
const banner = createBanner(commandName, options);
|
|
71
|
+
if (banner) {
|
|
72
|
+
const clack = require('@clack/prompts');
|
|
73
|
+
clack.intro(banner);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
createBanner,
|
|
79
|
+
showBanner,
|
|
80
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { createTable } = require('./table');
|
|
3
|
+
|
|
4
|
+
const STATUS_COLORS = {
|
|
5
|
+
M: chalk.yellow, // Modified
|
|
6
|
+
A: chalk.green, // Added
|
|
7
|
+
D: chalk.red, // Deleted
|
|
8
|
+
R: chalk.blue, // Renamed
|
|
9
|
+
C: chalk.magenta, // Copied
|
|
10
|
+
U: chalk.red, // Unmerged
|
|
11
|
+
'?': chalk.gray, // Untracked
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const STATUS_LABELS = {
|
|
15
|
+
M: 'Modified',
|
|
16
|
+
A: 'Added',
|
|
17
|
+
D: 'Deleted',
|
|
18
|
+
R: 'Renamed',
|
|
19
|
+
C: 'Copied',
|
|
20
|
+
U: 'Unmerged',
|
|
21
|
+
'?': 'Untracked',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Display repository status in a formatted way
|
|
26
|
+
*/
|
|
27
|
+
const displayStatus = (status, branch) => {
|
|
28
|
+
const sections = [];
|
|
29
|
+
|
|
30
|
+
// Branch info
|
|
31
|
+
if (branch) {
|
|
32
|
+
sections.push(chalk.cyan.bold(`\nOn branch ${branch}`));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Ahead/Behind info
|
|
36
|
+
if (status.ahead > 0 || status.behind > 0) {
|
|
37
|
+
const info = [];
|
|
38
|
+
if (status.ahead > 0) {
|
|
39
|
+
info.push(chalk.green(`${status.ahead} ahead`));
|
|
40
|
+
}
|
|
41
|
+
if (status.behind > 0) {
|
|
42
|
+
info.push(chalk.red(`${status.behind} behind`));
|
|
43
|
+
}
|
|
44
|
+
if (status.diverged) {
|
|
45
|
+
sections.push(chalk.yellow('Your branch has diverged from the remote'));
|
|
46
|
+
}
|
|
47
|
+
sections.push(` ${info.join(', ')} of origin/${branch}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Staged changes
|
|
51
|
+
if (status.staged.length > 0) {
|
|
52
|
+
sections.push(chalk.green.bold('\nChanges to be committed:'));
|
|
53
|
+
const rows = status.staged.map(({ status: stat, file }) => [
|
|
54
|
+
chalk.green(STATUS_LABELS[stat] || stat),
|
|
55
|
+
file,
|
|
56
|
+
]);
|
|
57
|
+
sections.push(createTable(['Status', 'File'], rows));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Unstaged changes
|
|
61
|
+
if (status.unstaged.length > 0) {
|
|
62
|
+
sections.push(chalk.yellow.bold('\nChanges not staged for commit:'));
|
|
63
|
+
const rows = status.unstaged.map(({ status: stat, file }) => [
|
|
64
|
+
chalk.yellow(STATUS_LABELS[stat] || stat),
|
|
65
|
+
file,
|
|
66
|
+
]);
|
|
67
|
+
sections.push(createTable(['Status', 'File'], rows));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Untracked files
|
|
71
|
+
if (status.untracked.length > 0) {
|
|
72
|
+
sections.push(chalk.gray.bold('\nUntracked files:'));
|
|
73
|
+
for (const file of status.untracked) {
|
|
74
|
+
sections.push(chalk.gray(` ${file}`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Clean working tree
|
|
79
|
+
if (status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0) {
|
|
80
|
+
sections.push(chalk.green('\nWorking tree clean'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return sections.join('\n');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
displayStatus,
|
|
88
|
+
STATUS_COLORS,
|
|
89
|
+
STATUS_LABELS,
|
|
90
|
+
};
|
package/lib/ui/table.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const Table = require('cli-table3');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a simple table display using cli-table3
|
|
6
|
+
*/
|
|
7
|
+
const createTable = (headers, rows, options = {}) => {
|
|
8
|
+
const { headerColor = 'cyan', align = 'left', style = {} } = options;
|
|
9
|
+
|
|
10
|
+
if (rows.length === 0) {
|
|
11
|
+
return chalk.dim('(empty)');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Create table with cli-table3
|
|
15
|
+
const table = new Table({
|
|
16
|
+
head: headers.map((h) => chalk[headerColor].bold(h)),
|
|
17
|
+
style: {
|
|
18
|
+
border: [],
|
|
19
|
+
head: [],
|
|
20
|
+
...style,
|
|
21
|
+
},
|
|
22
|
+
chars: {
|
|
23
|
+
top: '─',
|
|
24
|
+
'top-mid': '┬',
|
|
25
|
+
'top-left': '├',
|
|
26
|
+
'top-right': '┐',
|
|
27
|
+
bottom: '─',
|
|
28
|
+
'bottom-mid': '┴',
|
|
29
|
+
'bottom-left': '├',
|
|
30
|
+
'bottom-right': '┘',
|
|
31
|
+
left: '│',
|
|
32
|
+
'left-mid': '├',
|
|
33
|
+
mid: '─',
|
|
34
|
+
'mid-mid': '┼',
|
|
35
|
+
right: '│',
|
|
36
|
+
'right-mid': '┤',
|
|
37
|
+
middle: '│',
|
|
38
|
+
},
|
|
39
|
+
colAligns: headers.map(() => align),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Add rows
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
table.push(row);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Apply gray color to border characters
|
|
48
|
+
const tableString = table.toString();
|
|
49
|
+
// Match border characters (horizontal lines, vertical lines, corners, junctions)
|
|
50
|
+
// This regex matches border chars at the start/end of lines or as standalone border elements
|
|
51
|
+
const borderCharPattern = /([─┬┌┐┴└┘│├┼┤])/g;
|
|
52
|
+
const result = tableString.replace(borderCharPattern, (match) => chalk.gray(match));
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a key-value list
|
|
59
|
+
*/
|
|
60
|
+
const createKeyValueList = (items, options = {}) => {
|
|
61
|
+
const { keyColor = 'cyan', valueColor = 'white' } = options;
|
|
62
|
+
|
|
63
|
+
const maxKeyLength = Math.max(...Object.keys(items).map((k) => k.length));
|
|
64
|
+
|
|
65
|
+
return Object.entries(items)
|
|
66
|
+
.map(([key, value]) => {
|
|
67
|
+
const paddedKey = key.padEnd(maxKeyLength);
|
|
68
|
+
return `${chalk[keyColor](paddedKey)} : ${chalk[valueColor](value)}`;
|
|
69
|
+
})
|
|
70
|
+
.join('\n');
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
createTable,
|
|
75
|
+
createKeyValueList,
|
|
76
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const emailPrompt = require('email-prompt');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const clack = require('@clack/prompts');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prompt for email address with autocompletion
|
|
7
|
+
* @param {object} options - Email prompt options
|
|
8
|
+
* @returns {Promise<string>} - Email address
|
|
9
|
+
*/
|
|
10
|
+
const promptEmail = async (options = {}) => {
|
|
11
|
+
const {
|
|
12
|
+
start = '> Enter your email: ',
|
|
13
|
+
forceLowerCase = true,
|
|
14
|
+
suggestionColor = 'gray',
|
|
15
|
+
} = options;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const email = await emailPrompt({
|
|
19
|
+
start,
|
|
20
|
+
forceLowerCase,
|
|
21
|
+
suggestionColor,
|
|
22
|
+
});
|
|
23
|
+
return email;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err.message?.includes('Aborted')) {
|
|
26
|
+
clack.cancel(chalk.yellow('Email input cancelled'));
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Prompt for email with validation and confirmation
|
|
35
|
+
* @param {object} options - Options
|
|
36
|
+
* @returns {Promise<string|null>} - Email address or null if cancelled
|
|
37
|
+
*/
|
|
38
|
+
const promptEmailWithConfirmation = async (options = {}) => {
|
|
39
|
+
const email = await promptEmail(options);
|
|
40
|
+
|
|
41
|
+
if (!email) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Show confirmation
|
|
46
|
+
const confirm = await clack.confirm({
|
|
47
|
+
message: `Use email: ${chalk.cyan(email)}?`,
|
|
48
|
+
initialValue: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (clack.isCancel(confirm) || !confirm) {
|
|
52
|
+
clack.cancel(chalk.yellow('Email not set'));
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return email;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
promptEmail,
|
|
61
|
+
promptEmailWithConfirmation,
|
|
62
|
+
};
|