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.
Files changed (51) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +459 -0
  3. package/cli.js +342 -0
  4. package/commands/add.js +159 -0
  5. package/commands/blame.js +33 -0
  6. package/commands/branch.js +234 -0
  7. package/commands/checkout.js +43 -0
  8. package/commands/cherry-pick.js +104 -0
  9. package/commands/clean.js +71 -0
  10. package/commands/clone.js +76 -0
  11. package/commands/commit.js +82 -0
  12. package/commands/config.js +171 -0
  13. package/commands/diff.js +30 -0
  14. package/commands/fetch.js +76 -0
  15. package/commands/grep.js +42 -0
  16. package/commands/init.js +45 -0
  17. package/commands/log.js +38 -0
  18. package/commands/merge.js +69 -0
  19. package/commands/mv.js +40 -0
  20. package/commands/pull.js +74 -0
  21. package/commands/push.js +97 -0
  22. package/commands/rebase.js +134 -0
  23. package/commands/remote.js +236 -0
  24. package/commands/restore.js +76 -0
  25. package/commands/revert.js +63 -0
  26. package/commands/rm.js +57 -0
  27. package/commands/show.js +47 -0
  28. package/commands/stash.js +201 -0
  29. package/commands/status.js +21 -0
  30. package/commands/sync.js +98 -0
  31. package/commands/tag.js +153 -0
  32. package/commands/undo.js +200 -0
  33. package/commands/uninit.js +57 -0
  34. package/index.d.ts +56 -0
  35. package/index.js +55 -0
  36. package/lib/commit/build-commit.js +64 -0
  37. package/lib/commit/get-previous-commit.js +15 -0
  38. package/lib/commit/questions.js +226 -0
  39. package/lib/config/read-config-file.js +54 -0
  40. package/lib/git/exec.js +222 -0
  41. package/lib/ui/ascii.js +154 -0
  42. package/lib/ui/banner.js +80 -0
  43. package/lib/ui/status-display.js +90 -0
  44. package/lib/ui/table.js +76 -0
  45. package/lib/utils/email-prompt.js +62 -0
  46. package/lib/utils/logger.js +47 -0
  47. package/lib/utils/spinner.js +57 -0
  48. package/lib/utils/terminal-link.js +55 -0
  49. package/lib/versions.js +17 -0
  50. package/package.json +73 -0
  51. package/standalone.js +24 -0
@@ -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
+ };
@@ -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
+ };
@@ -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
+ };
@@ -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
+ };