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
package/cli.js ADDED
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+
3
+ const clack = require('@clack/prompts');
4
+ const chalk = require('chalk');
5
+ const { isGitRepo } = require('./lib/git/exec');
6
+
7
+ // Command aliases mapping
8
+ const COMMANDS = {
9
+ status: require('./commands/status'),
10
+ st: require('./commands/status'),
11
+
12
+ branch: require('./commands/branch'),
13
+ br: require('./commands/branch'),
14
+ co: require('./commands/branch'),
15
+
16
+ commit: require('./commands/commit'),
17
+ ci: require('./commands/commit'),
18
+
19
+ pull: require('./commands/pull'),
20
+ pl: require('./commands/pull'),
21
+
22
+ push: require('./commands/push'),
23
+ ps: require('./commands/push'),
24
+
25
+ sync: require('./commands/sync'),
26
+
27
+ merge: require('./commands/merge'),
28
+
29
+ rebase: require('./commands/rebase'),
30
+
31
+ stash: require('./commands/stash'),
32
+
33
+ log: require('./commands/log'),
34
+
35
+ undo: require('./commands/undo'),
36
+ reset: require('./commands/undo'),
37
+
38
+ // New commands
39
+ add: require('./commands/add'),
40
+ diff: require('./commands/diff'),
41
+ init: require('./commands/init'),
42
+ uninit: require('./commands/uninit'),
43
+ deinit: require('./commands/uninit'),
44
+ clone: require('./commands/clone'),
45
+ fetch: require('./commands/fetch'),
46
+ remote: require('./commands/remote'),
47
+ tag: require('./commands/tag'),
48
+ show: require('./commands/show'),
49
+ checkout: require('./commands/checkout'),
50
+ revert: require('./commands/revert'),
51
+ 'cherry-pick': require('./commands/cherry-pick'),
52
+ clean: require('./commands/clean'),
53
+ config: require('./commands/config'),
54
+ blame: require('./commands/blame'),
55
+ grep: require('./commands/grep'),
56
+ rm: require('./commands/rm'),
57
+ mv: require('./commands/mv'),
58
+ restore: require('./commands/restore'),
59
+ };
60
+
61
+ const VERSION = require('./package.json').version;
62
+ const { showBanner } = require('./lib/ui/banner');
63
+ const { createLink } = require('./lib/utils/terminal-link');
64
+
65
+ // Command categories
66
+ const COMMAND_CATEGORIES = {
67
+ core: {
68
+ name: 'Core Commands',
69
+ commands: [
70
+ { name: 'status', aliases: ['st'], description: 'Show repository status' },
71
+ {
72
+ name: 'branch',
73
+ aliases: ['br', 'co'],
74
+ description: 'Branch management (list, create, checkout, delete)',
75
+ },
76
+ { name: 'commit', aliases: ['ci'], description: 'Create commits with conventional format' },
77
+ { name: 'pull', aliases: ['pl'], description: 'Fetch and merge from remote' },
78
+ { name: 'push', aliases: ['ps'], description: 'Push to remote' },
79
+ { name: 'sync', aliases: [], description: 'Synchronize (pull + rebase + push)' },
80
+ { name: 'merge', aliases: [], description: 'Merge branches' },
81
+ { name: 'rebase', aliases: [], description: 'Rebase operations' },
82
+ { name: 'stash', aliases: [], description: 'Stash management' },
83
+ { name: 'log', aliases: [], description: 'View commit history' },
84
+ { name: 'undo', aliases: ['reset'], description: 'Undo operations and reflog browser' },
85
+ ],
86
+ },
87
+ file: {
88
+ name: 'File Operations',
89
+ commands: [
90
+ { name: 'add', aliases: [], description: 'Stage files for commit' },
91
+ { name: 'diff', aliases: [], description: 'Show changes' },
92
+ { name: 'checkout', aliases: [], description: 'Checkout files' },
93
+ { name: 'restore', aliases: [], description: 'Restore files from index or commit' },
94
+ { name: 'rm', aliases: [], description: 'Remove files from git' },
95
+ { name: 'mv', aliases: [], description: 'Move/rename files' },
96
+ { name: 'clean', aliases: [], description: 'Remove untracked files' },
97
+ ],
98
+ },
99
+ repo: {
100
+ name: 'Repository Management',
101
+ commands: [
102
+ { name: 'init', aliases: [], description: 'Initialize a new repository' },
103
+ { name: 'uninit', aliases: ['deinit'], description: 'Remove git repository (clear history)' },
104
+ { name: 'clone', aliases: [], description: 'Clone a repository' },
105
+ { name: 'remote', aliases: [], description: 'Manage remote repositories' },
106
+ { name: 'fetch', aliases: [], description: 'Fetch from remote' },
107
+ { name: 'tag', aliases: [], description: 'Tag management' },
108
+ { name: 'config', aliases: [], description: 'Git configuration management' },
109
+ ],
110
+ },
111
+ history: {
112
+ name: 'History & Inspection',
113
+ commands: [
114
+ { name: 'show', aliases: [], description: 'Show commit details' },
115
+ { name: 'revert', aliases: [], description: 'Revert commits' },
116
+ { name: 'cherry-pick', aliases: [], description: 'Apply commits from another branch' },
117
+ { name: 'blame', aliases: [], description: 'Show who last modified each line' },
118
+ { name: 'grep', aliases: [], description: 'Search in repository' },
119
+ ],
120
+ },
121
+ };
122
+
123
+ const showHelp = () => {
124
+ // Show banner
125
+ showBanner('GITTABLE', { version: VERSION });
126
+
127
+ // GitHub link at the top
128
+ const repoLink = createLink('GitHub', 'https://github.com/GG-Santos/Gittable');
129
+ console.log(`${chalk.gray('├')} ${chalk.dim(repoLink)}`);
130
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Modern Git CLI with Interactive Prompts')}`);
131
+ console.log(
132
+ chalk.gray('├') +
133
+ ' ' +
134
+ chalk.yellow('Usage:') +
135
+ ' ' +
136
+ chalk.white('gittable <command> [options]')
137
+ );
138
+ console.log(chalk.gray('│'));
139
+
140
+ // Core Commands
141
+ console.log(chalk.bold.cyan(' Core Commands:'));
142
+ console.log();
143
+ for (const cmd of COMMAND_CATEGORIES.core.commands) {
144
+ const cmdList = [cmd.name, ...cmd.aliases].join(', ');
145
+ console.log(` ${chalk.cyan(cmdList.padEnd(20))} ${chalk.gray(cmd.description)}`);
146
+ }
147
+ console.log();
148
+
149
+ // File Operations
150
+ console.log(chalk.bold.cyan(' File Operations:'));
151
+ console.log();
152
+ for (const cmd of COMMAND_CATEGORIES.file.commands) {
153
+ const cmdList = [cmd.name, ...cmd.aliases].join(', ');
154
+ console.log(` ${chalk.cyan(cmdList.padEnd(20))} ${chalk.gray(cmd.description)}`);
155
+ }
156
+ console.log();
157
+
158
+ // Repository Management
159
+ console.log(chalk.bold.cyan(' Repository Management:'));
160
+ console.log();
161
+ for (const cmd of COMMAND_CATEGORIES.repo.commands) {
162
+ const cmdList = [cmd.name, ...cmd.aliases].join(', ');
163
+ console.log(` ${chalk.cyan(cmdList.padEnd(20))} ${chalk.gray(cmd.description)}`);
164
+ }
165
+ console.log();
166
+
167
+ // History & Inspection
168
+ console.log(chalk.bold.cyan(' History & Inspection:'));
169
+ console.log();
170
+ for (const cmd of COMMAND_CATEGORIES.history.commands) {
171
+ const cmdList = [cmd.name, ...cmd.aliases].join(', ');
172
+ console.log(` ${chalk.cyan(cmdList.padEnd(20))} ${chalk.gray(cmd.description)}`);
173
+ }
174
+ console.log();
175
+ };
176
+
177
+ const showInteractiveMenu = async () => {
178
+ showBanner('GITTABLE', { version: VERSION });
179
+
180
+ const category = await clack.select({
181
+ message: chalk.cyan('Select a category:'),
182
+ options: [
183
+ { value: 'core', label: chalk.cyan('Core Commands') },
184
+ { value: 'file', label: chalk.cyan('File Operations') },
185
+ { value: 'repo', label: chalk.cyan('Repository Management') },
186
+ { value: 'history', label: chalk.cyan('History & Inspection') },
187
+ { value: 'help', label: chalk.yellow('List Commands') },
188
+ { value: 'exit', label: chalk.red('Exit') },
189
+ ],
190
+ });
191
+
192
+ if (clack.isCancel(category) || category === 'exit') {
193
+ clack.cancel(chalk.yellow('Cancelled'));
194
+ return;
195
+ }
196
+
197
+ if (category === 'help') {
198
+ showHelp();
199
+ return;
200
+ }
201
+
202
+ // Show commands in selected category
203
+ const categoryData = COMMAND_CATEGORIES[category];
204
+ const commandOptions = categoryData.commands.map((cmd) => ({
205
+ value: cmd.name,
206
+ label: `${chalk.cyan(cmd.name)} ${chalk.gray(`- ${cmd.description}`)}`,
207
+ }));
208
+
209
+ // Add back option
210
+ commandOptions.push({
211
+ value: '__back__',
212
+ label: chalk.dim('← Back to categories'),
213
+ });
214
+
215
+ const selectedCommand = await clack.select({
216
+ message: chalk.cyan(`Select a command from ${categoryData.name}:`),
217
+ options: commandOptions,
218
+ });
219
+
220
+ if (clack.isCancel(selectedCommand) || selectedCommand === '__back__') {
221
+ // Recursively show menu again
222
+ return showInteractiveMenu();
223
+ }
224
+
225
+ // Execute the selected command
226
+ if (COMMANDS[selectedCommand]) {
227
+ // Check if we're in a git repo (except for init, uninit, and clone)
228
+ if (
229
+ !isGitRepo() &&
230
+ selectedCommand !== 'init' &&
231
+ selectedCommand !== 'uninit' &&
232
+ selectedCommand !== 'clone'
233
+ ) {
234
+ clack.cancel(chalk.red('Not a git repository'));
235
+ console.log();
236
+ console.log(`${chalk.gray('├')} ${chalk.yellow('Tip:')}`);
237
+ console.log(`${chalk.gray('│')} ${chalk.gray('Initialize a new repository with:')}`);
238
+ console.log(`${chalk.gray('│')} ${chalk.cyan(' gittable init')}`);
239
+ console.log(chalk.gray('│'));
240
+ console.log(`${chalk.gray('└')} ${chalk.gray('Or clone an existing repository with:')}`);
241
+ console.log(chalk.gray(' ') + chalk.cyan(' gittable clone <url>'));
242
+ console.log();
243
+ return;
244
+ }
245
+
246
+ try {
247
+ await COMMANDS[selectedCommand]([]);
248
+ } catch (error) {
249
+ clack.cancel(chalk.red('Command failed'));
250
+ console.error(error);
251
+ process.exit(1);
252
+ }
253
+ } else {
254
+ clack.cancel(chalk.red(`Command '${selectedCommand}' not found`));
255
+ process.exit(1);
256
+ }
257
+ };
258
+
259
+ const main = async () => {
260
+ const args = process.argv.slice(2);
261
+
262
+ if (args[0] === '--help' || args[0] === '-h') {
263
+ showHelp();
264
+ process.exit(0);
265
+ }
266
+
267
+ if (args[0] === '--version' || args[0] === '-v') {
268
+ showBanner('GITTABLE', { version: VERSION });
269
+ console.log();
270
+ clack.outro(chalk.green(`Version ${VERSION}`));
271
+ process.exit(0);
272
+ }
273
+
274
+ // If no arguments provided and TTY is available, show interactive menu
275
+ if (args.length === 0 && process.stdin.isTTY) {
276
+ try {
277
+ await showInteractiveMenu();
278
+ process.exit(0);
279
+ } catch (error) {
280
+ clack.cancel(chalk.red('Menu failed'));
281
+ console.error(error);
282
+ process.exit(1);
283
+ }
284
+ }
285
+
286
+ // If no arguments and not TTY, show help
287
+ if (args.length === 0) {
288
+ showHelp();
289
+ process.exit(0);
290
+ }
291
+
292
+ const command = args[0].toLowerCase();
293
+ const commandArgs = args.slice(1);
294
+
295
+ if (!COMMANDS[command]) {
296
+ showBanner('GITTABLE', { version: VERSION });
297
+ console.log();
298
+ clack.cancel(chalk.red(`Unknown command: ${chalk.bold(command)}`));
299
+ console.log();
300
+ console.log(`${chalk.gray('├')} ${chalk.yellow('Available commands:')}`);
301
+ console.log(chalk.gray('│'));
302
+
303
+ // Show some common commands as suggestions
304
+ const suggestions = Object.keys(COMMANDS).slice(0, 10);
305
+ for (const cmd of suggestions) {
306
+ console.log(`${chalk.gray('│')} ${chalk.cyan(` ${cmd}`)}`);
307
+ }
308
+
309
+ console.log(chalk.gray('│'));
310
+ console.log(
311
+ `${chalk.gray('└')} ${chalk.gray('Run "gittable --help" for full usage information')}`
312
+ );
313
+ console.log();
314
+ process.exit(1);
315
+ }
316
+
317
+ // Check if we're in a git repo (except for help/version, init, uninit, and clone)
318
+ if (!isGitRepo() && command !== 'init' && command !== 'uninit' && command !== 'clone') {
319
+ showBanner('GITTABLE', { version: VERSION });
320
+ console.log();
321
+ clack.cancel(chalk.red('Not a git repository'));
322
+ console.log();
323
+ console.log(`${chalk.gray('├')} ${chalk.yellow('Tip:')}`);
324
+ console.log(`${chalk.gray('│')} ${chalk.gray('Initialize a new repository with:')}`);
325
+ console.log(`${chalk.gray('│')} ${chalk.cyan(' gittable init')}`);
326
+ console.log(chalk.gray('│'));
327
+ console.log(`${chalk.gray('└')} ${chalk.gray('Or clone an existing repository with:')}`);
328
+ console.log(chalk.gray(' ') + chalk.cyan(' gittable clone <url>'));
329
+ console.log();
330
+ process.exit(1);
331
+ }
332
+
333
+ try {
334
+ await COMMANDS[command](commandArgs);
335
+ } catch (error) {
336
+ clack.cancel(chalk.red('Command failed'));
337
+ console.error(error);
338
+ process.exit(1);
339
+ }
340
+ };
341
+
342
+ main();
@@ -0,0 +1,159 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const { execGit, getStatus } = require('../lib/git/exec');
4
+ const { showBanner } = require('../lib/ui/banner');
5
+
6
+ const stageFiles = async (files) => {
7
+ if (!files || files.length === 0) {
8
+ // Check if TTY is available for interactive prompts
9
+ if (!process.stdin.isTTY) {
10
+ clack.cancel(chalk.red('Interactive mode required'));
11
+ console.log(chalk.yellow('This command requires interactive input.'));
12
+ console.log(chalk.gray('Available options:'));
13
+ console.log(chalk.gray(' - gittable add <file1> <file2> ...'));
14
+ console.log(chalk.gray(' - gittable add --all (or -A)'));
15
+ process.exit(1);
16
+ }
17
+
18
+ // Interactive mode: show unstaged files
19
+ const status = getStatus();
20
+ if (!status) {
21
+ clack.cancel(chalk.red('Failed to get repository status'));
22
+ process.exit(1);
23
+ }
24
+
25
+ const allFiles = [
26
+ ...status.unstaged.map((f) => ({
27
+ value: f.file,
28
+ label: chalk.yellow(`M ${f.file}`),
29
+ hint: 'modified',
30
+ })),
31
+ ...status.untracked.map((f) => ({
32
+ value: f,
33
+ label: chalk.green(`? ${f}`),
34
+ hint: 'untracked',
35
+ })),
36
+ ];
37
+
38
+ if (allFiles.length === 0) {
39
+ clack.cancel(chalk.yellow('No files to stage'));
40
+ return;
41
+ }
42
+
43
+ const selected = await clack.multiselect({
44
+ message: chalk.cyan('Select files to stage:'),
45
+ options: allFiles,
46
+ });
47
+
48
+ if (clack.isCancel(selected)) {
49
+ clack.cancel(chalk.yellow('Cancelled'));
50
+ return;
51
+ }
52
+
53
+ files = selected;
54
+ }
55
+
56
+ const spinner = clack.spinner();
57
+ spinner.start(`Staging ${files.length} file(s)`);
58
+
59
+ const result = execGit(`add ${files.join(' ')}`, { silent: false });
60
+ spinner.stop();
61
+
62
+ if (result.success) {
63
+ clack.outro(chalk.green.bold(`Staged ${files.length} file(s)`));
64
+ } else {
65
+ clack.cancel(chalk.red('Failed to stage files'));
66
+ console.error(result.error);
67
+ process.exit(1);
68
+ }
69
+ };
70
+
71
+ const stageAll = async () => {
72
+ const confirm = await clack.confirm({
73
+ message: chalk.cyan('Stage all changes?'),
74
+ initialValue: false,
75
+ });
76
+
77
+ if (clack.isCancel(confirm) || !confirm) {
78
+ clack.cancel(chalk.yellow('Cancelled'));
79
+ return;
80
+ }
81
+
82
+ const spinner = clack.spinner();
83
+ spinner.start('Staging all changes');
84
+
85
+ const result = execGit('add -A', { silent: false });
86
+ spinner.stop();
87
+
88
+ if (result.success) {
89
+ clack.outro(chalk.green.bold('Staged all changes'));
90
+ } else {
91
+ clack.cancel(chalk.red('Failed to stage files'));
92
+ console.error(result.error);
93
+ process.exit(1);
94
+ }
95
+ };
96
+
97
+ const unstageFiles = async (files) => {
98
+ if (!files || files.length === 0) {
99
+ const status = getStatus();
100
+ if (!status || status.staged.length === 0) {
101
+ clack.cancel(chalk.yellow('No staged files to unstage'));
102
+ return;
103
+ }
104
+
105
+ const options = status.staged.map((f) => ({
106
+ value: f.file,
107
+ label: `${f.status} ${f.file}`,
108
+ }));
109
+
110
+ const selected = await clack.multiselect({
111
+ message: chalk.cyan('Select files to unstage:'),
112
+ options,
113
+ });
114
+
115
+ if (clack.isCancel(selected)) {
116
+ clack.cancel(chalk.yellow('Cancelled'));
117
+ return;
118
+ }
119
+
120
+ files = selected;
121
+ }
122
+
123
+ const spinner = clack.spinner();
124
+ spinner.start(`Unstaging ${files.length} file(s)`);
125
+
126
+ const result = execGit(`reset HEAD -- ${files.join(' ')}`, { silent: false });
127
+ spinner.stop();
128
+
129
+ if (result.success) {
130
+ clack.outro(chalk.green.bold(`Unstaged ${files.length} file(s)`));
131
+ } else {
132
+ clack.cancel(chalk.red('Failed to unstage files'));
133
+ console.error(result.error);
134
+ process.exit(1);
135
+ }
136
+ };
137
+
138
+ module.exports = async (args) => {
139
+ const action = args[0];
140
+
141
+ if (action === '--all' || action === '-A') {
142
+ showBanner('ADD');
143
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Stage All Changes')}`);
144
+ await stageAll();
145
+ return;
146
+ }
147
+
148
+ if (action === '--unstage' || action === 'unstage') {
149
+ showBanner('ADD');
150
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Unstage Files')}`);
151
+ await unstageFiles(args.slice(1));
152
+ return;
153
+ }
154
+
155
+ // Default: stage files
156
+ showBanner('ADD');
157
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Stage Files')}`);
158
+ await stageFiles(args);
159
+ };
@@ -0,0 +1,33 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const { execGit } = require('../lib/git/exec');
4
+ const { showBanner } = require('../lib/ui/banner');
5
+
6
+ module.exports = async (args) => {
7
+ showBanner('BLAME');
8
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Blame File')}`);
9
+
10
+ const file = args[0];
11
+ const revision = args.find((arg) => !arg.startsWith('--'));
12
+
13
+ if (!file) {
14
+ clack.cancel(chalk.yellow('No file specified'));
15
+ return;
16
+ }
17
+
18
+ let command = 'blame';
19
+ if (revision && revision !== file) {
20
+ command += ` ${revision}`;
21
+ }
22
+ command += ` ${file}`;
23
+
24
+ const result = execGit(command, { silent: false });
25
+
26
+ if (!result.success) {
27
+ clack.cancel(chalk.red('Failed to show blame'));
28
+ console.error(result.error);
29
+ process.exit(1);
30
+ }
31
+
32
+ clack.outro(chalk.green.bold('Done'));
33
+ };