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,171 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const { execGit } = require('../lib/git/exec');
4
+ const { createTable } = require('../lib/ui/table');
5
+ const { showBanner } = require('../lib/ui/banner');
6
+
7
+ const listConfig = (scope = 'local') => {
8
+ const scopeFlag = scope === 'global' ? '--global' : scope === 'system' ? '--system' : '--local';
9
+ const result = execGit(`config ${scopeFlag} --list`, { silent: true });
10
+
11
+ if (!result.success || !result.output.trim()) {
12
+ console.log(chalk.dim('No configuration found'));
13
+ return;
14
+ }
15
+
16
+ const lines = result.output.trim().split('\n').filter(Boolean);
17
+ const configs = lines.map((line) => {
18
+ const [key, ...valueParts] = line.split('=');
19
+ return {
20
+ key,
21
+ value: valueParts.join('='),
22
+ };
23
+ });
24
+
25
+ const rows = configs.map((config) => [chalk.cyan(config.key), config.value]);
26
+
27
+ console.log(`\n${createTable(['Key', 'Value'], rows)}`);
28
+ };
29
+
30
+ const getConfig = async (key, scope = 'local') => {
31
+ const scopeFlag = scope === 'global' ? '--global' : scope === 'system' ? '--system' : '--local';
32
+ const result = execGit(`config ${scopeFlag} --get ${key}`, { silent: true });
33
+
34
+ if (result.success) {
35
+ console.log(chalk.green(result.output.trim()));
36
+ } else {
37
+ clack.cancel(chalk.yellow(`Config key "${key}" not found`));
38
+ }
39
+ };
40
+
41
+ const setConfig = async (key, value, scope = 'local') => {
42
+ if (!key) {
43
+ key = await clack.text({
44
+ message: chalk.cyan('Config key:'),
45
+ placeholder: 'user.name',
46
+ });
47
+
48
+ if (clack.isCancel(key)) {
49
+ clack.cancel(chalk.yellow('Cancelled'));
50
+ return;
51
+ }
52
+ }
53
+
54
+ if (!value) {
55
+ value = await clack.text({
56
+ message: chalk.cyan('Config value:'),
57
+ placeholder: 'John Doe',
58
+ });
59
+
60
+ if (clack.isCancel(value)) {
61
+ clack.cancel(chalk.yellow('Cancelled'));
62
+ return;
63
+ }
64
+ }
65
+
66
+ const spinner = clack.spinner();
67
+ spinner.start(`Setting config ${key}`);
68
+
69
+ const scopeFlag = scope === 'global' ? '--global' : scope === 'system' ? '--system' : '--local';
70
+ const result = execGit(`config ${scopeFlag} ${key} "${value}"`, { silent: true });
71
+ spinner.stop();
72
+
73
+ if (result.success) {
74
+ clack.outro(chalk.green.bold(`Config ${key} set to ${value}`));
75
+ } else {
76
+ clack.cancel(chalk.red('Failed to set config'));
77
+ console.error(result.error);
78
+ process.exit(1);
79
+ }
80
+ };
81
+
82
+ const unsetConfig = async (key, scope = 'local') => {
83
+ if (!key) {
84
+ key = await clack.text({
85
+ message: chalk.cyan('Config key to unset:'),
86
+ placeholder: 'user.name',
87
+ });
88
+
89
+ if (clack.isCancel(key)) {
90
+ clack.cancel(chalk.yellow('Cancelled'));
91
+ return;
92
+ }
93
+ }
94
+
95
+ const confirm = await clack.confirm({
96
+ message: chalk.yellow(`Unset config ${key}?`),
97
+ initialValue: false,
98
+ });
99
+
100
+ if (clack.isCancel(confirm) || !confirm) {
101
+ clack.cancel(chalk.yellow('Cancelled'));
102
+ return;
103
+ }
104
+
105
+ const spinner = clack.spinner();
106
+ spinner.start(`Unsetting config ${key}`);
107
+
108
+ const scopeFlag = scope === 'global' ? '--global' : scope === 'system' ? '--system' : '--local';
109
+ const result = execGit(`config ${scopeFlag} --unset ${key}`, { silent: true });
110
+ spinner.stop();
111
+
112
+ if (result.success) {
113
+ clack.outro(chalk.green.bold(`Config ${key} unset`));
114
+ } else {
115
+ clack.cancel(chalk.red('Failed to unset config'));
116
+ console.error(result.error);
117
+ process.exit(1);
118
+ }
119
+ };
120
+
121
+ module.exports = async (args) => {
122
+ const action = args[0];
123
+ const global = args.includes('--global') || args.includes('-g');
124
+ const system = args.includes('--system') || args.includes('-s');
125
+ const scope = global ? 'global' : system ? 'system' : 'local';
126
+
127
+ if (!action || action === 'list' || action === 'ls') {
128
+ showBanner('CONFIG');
129
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold(`Config List (${scope})`)}`);
130
+ listConfig(scope);
131
+ clack.outro(chalk.green.bold('Done'));
132
+ return;
133
+ }
134
+
135
+ if (action === 'get') {
136
+ showBanner('CONFIG');
137
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Get Config')}`);
138
+ await getConfig(args[1], scope);
139
+ clack.outro(chalk.green.bold('Done'));
140
+ return;
141
+ }
142
+
143
+ if (action === 'set') {
144
+ showBanner('CONFIG');
145
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Set Config')}`);
146
+ await setConfig(args[1], args[2], scope);
147
+ return;
148
+ }
149
+
150
+ if (action === 'unset' || action === 'remove' || action === 'rm') {
151
+ showBanner('CONFIG');
152
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Unset Config')}`);
153
+ await unsetConfig(args[1], scope);
154
+ return;
155
+ }
156
+
157
+ // If action looks like a key=value, treat as set
158
+ if (action.includes('=')) {
159
+ const [key, ...valueParts] = action.split('=');
160
+ showBanner('CONFIG');
161
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Set Config')}`);
162
+ await setConfig(key, valueParts.join('='), scope);
163
+ return;
164
+ }
165
+
166
+ // Default: get config
167
+ showBanner('CONFIG');
168
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Get Config')}`);
169
+ await getConfig(action, scope);
170
+ clack.outro(chalk.green.bold('Done'));
171
+ };
@@ -0,0 +1,30 @@
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('DIFF');
8
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Show Changes')}`);
9
+
10
+ const staged = args.includes('--staged') || args.includes('--cached');
11
+ const file = args.find((arg) => !arg.startsWith('--'));
12
+
13
+ let command = 'diff';
14
+ if (staged) {
15
+ command += ' --staged';
16
+ }
17
+ if (file) {
18
+ command += ` ${file}`;
19
+ }
20
+
21
+ const result = execGit(command, { silent: false });
22
+
23
+ if (!result.success) {
24
+ clack.cancel(chalk.red('Failed to show diff'));
25
+ console.error(result.error);
26
+ process.exit(1);
27
+ }
28
+
29
+ clack.outro(chalk.green.bold('Done'));
30
+ };
@@ -0,0 +1,76 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const { execGit, remoteExists, getRemotes } = require('../lib/git/exec');
4
+ const { showBanner } = require('../lib/ui/banner');
5
+ const { addRemote } = require('./remote');
6
+
7
+ module.exports = async (args) => {
8
+ showBanner('FETCH');
9
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Fetch from Remote')}`);
10
+
11
+ let remote = args[0] || 'origin';
12
+ const all = args.includes('--all') || args.includes('-a');
13
+ const prune = args.includes('--prune') || args.includes('-p');
14
+
15
+ if (!all && !remoteExists(remote)) {
16
+ const remotes = getRemotes();
17
+ if (remotes.length === 0) {
18
+ console.log(chalk.yellow(`Remote '${remote}' does not exist.`));
19
+ const shouldAdd = await clack.confirm({
20
+ message: chalk.cyan(`Would you like to add remote '${remote}'?`),
21
+ initialValue: true,
22
+ });
23
+
24
+ if (clack.isCancel(shouldAdd) || !shouldAdd) {
25
+ clack.cancel(chalk.yellow('Cancelled'));
26
+ process.exit(1);
27
+ }
28
+
29
+ const added = await addRemote(remote, null);
30
+ if (!added) {
31
+ process.exit(1);
32
+ }
33
+ } else {
34
+ console.log(chalk.yellow(`Remote '${remote}' not found`));
35
+ console.log(chalk.dim(`Available remotes: ${remotes.join(', ')}`));
36
+ const shouldAdd = await clack.confirm({
37
+ message: chalk.cyan(`Would you like to add remote '${remote}'?`),
38
+ initialValue: true,
39
+ });
40
+
41
+ if (clack.isCancel(shouldAdd) || !shouldAdd) {
42
+ clack.cancel(chalk.yellow('Cancelled'));
43
+ process.exit(1);
44
+ }
45
+
46
+ const added = await addRemote(remote, null);
47
+ if (!added) {
48
+ process.exit(1);
49
+ }
50
+ }
51
+ }
52
+
53
+ const spinner = clack.spinner();
54
+ spinner.start(`Fetching from ${all ? 'all remotes' : remote}`);
55
+
56
+ let command = 'fetch';
57
+ if (all) {
58
+ command += ' --all';
59
+ } else {
60
+ command += ` ${remote}`;
61
+ }
62
+ if (prune) {
63
+ command += ' --prune';
64
+ }
65
+
66
+ const result = execGit(command, { silent: false });
67
+ spinner.stop();
68
+
69
+ if (result.success) {
70
+ clack.outro(chalk.green.bold('Fetch completed'));
71
+ } else {
72
+ clack.cancel(chalk.red('Fetch failed'));
73
+ console.error(result.error);
74
+ process.exit(1);
75
+ }
76
+ };
@@ -0,0 +1,42 @@
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('GREP');
8
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Search in Repository')}`);
9
+
10
+ const pattern = args[0];
11
+ const caseInsensitive = args.includes('--ignore-case') || args.includes('-i');
12
+ const files = args.filter((arg) => !arg.startsWith('--') && arg !== pattern);
13
+
14
+ if (!pattern) {
15
+ clack.cancel(chalk.yellow('No search pattern specified'));
16
+ return;
17
+ }
18
+
19
+ let command = 'grep';
20
+ if (caseInsensitive) {
21
+ command += ' --ignore-case';
22
+ }
23
+ command += ` "${pattern}"`;
24
+ if (files.length > 0) {
25
+ command += ` ${files.join(' ')}`;
26
+ }
27
+
28
+ const result = execGit(command, { silent: false });
29
+
30
+ if (!result.success) {
31
+ // Grep returns non-zero when no matches found, which is not an error
32
+ if (result.error && !result.error.includes('No matches found')) {
33
+ clack.cancel(chalk.red('Grep failed'));
34
+ console.error(result.error);
35
+ process.exit(1);
36
+ } else {
37
+ console.log(chalk.dim('No matches found'));
38
+ }
39
+ }
40
+
41
+ clack.outro(chalk.green.bold('Done'));
42
+ };
@@ -0,0 +1,45 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const { execGit, isGitRepo } = require('../lib/git/exec');
4
+ const { showBanner } = require('../lib/ui/banner');
5
+ const _path = require('node:path');
6
+
7
+ module.exports = async (args) => {
8
+ showBanner('INIT');
9
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Initialize Repository')}`);
10
+
11
+ if (isGitRepo()) {
12
+ clack.cancel(chalk.yellow('Already a git repository'));
13
+ return;
14
+ }
15
+
16
+ const dir = args[0] || '.';
17
+ const bare = args.includes('--bare');
18
+ const initialBranch =
19
+ args.find((arg) => arg.startsWith('--initial-branch='))?.split('=')[1] || 'main';
20
+
21
+ const spinner = clack.spinner();
22
+ spinner.start(`Initializing repository${dir !== '.' ? ` in ${dir}` : ''}`);
23
+
24
+ let command = `init`;
25
+ if (bare) {
26
+ command += ' --bare';
27
+ }
28
+ if (initialBranch) {
29
+ command += ` --initial-branch=${initialBranch}`;
30
+ }
31
+ if (dir !== '.') {
32
+ command += ` ${dir}`;
33
+ }
34
+
35
+ const result = execGit(command, { silent: true });
36
+ spinner.stop();
37
+
38
+ if (result.success) {
39
+ clack.outro(chalk.green.bold('Repository initialized'));
40
+ } else {
41
+ clack.cancel(chalk.red('Failed to initialize repository'));
42
+ console.error(result.error);
43
+ process.exit(1);
44
+ }
45
+ };
@@ -0,0 +1,38 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const { getLog } = require('../lib/git/exec');
4
+ const { createTable } = require('../lib/ui/table');
5
+ const { showBanner } = require('../lib/ui/banner');
6
+
7
+ module.exports = async (args) => {
8
+ showBanner('LOG');
9
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Commit History')}`);
10
+
11
+ const limit = parseInt(args[0], 10) || 20;
12
+ const format = args.includes('--oneline') ? '%h|%s' : '%h|%an|%ar|%s';
13
+
14
+ const commits = getLog(limit, format);
15
+
16
+ if (commits.length === 0) {
17
+ console.log(chalk.dim('No commits found'));
18
+ clack.outro(chalk.green.bold('Done'));
19
+ return;
20
+ }
21
+
22
+ if (args.includes('--oneline')) {
23
+ for (const commit of commits) {
24
+ console.log(`${chalk.cyan(commit.hash)} ${commit.message}`);
25
+ }
26
+ } else {
27
+ const rows = commits.map((commit) => [
28
+ chalk.cyan(commit.hash),
29
+ commit.author,
30
+ chalk.gray(commit.date),
31
+ commit.message,
32
+ ]);
33
+
34
+ console.log(`\n${createTable(['Hash', 'Author', 'Date', 'Message'], rows)}`);
35
+ }
36
+
37
+ clack.outro(chalk.green.bold('Done'));
38
+ };
@@ -0,0 +1,69 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const { execGit, getBranches, getCurrentBranch } = require('../lib/git/exec');
4
+ const { showBanner } = require('../lib/ui/banner');
5
+
6
+ module.exports = async (args) => {
7
+ showBanner('MERGE');
8
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Merge Branch')}`);
9
+
10
+ const currentBranch = getCurrentBranch();
11
+ const branches = getBranches();
12
+
13
+ let branchToMerge = args[0];
14
+
15
+ if (!branchToMerge) {
16
+ // Check if TTY is available for interactive prompts
17
+ if (!process.stdin.isTTY) {
18
+ clack.cancel(chalk.red('Interactive mode required'));
19
+ console.log(chalk.yellow('This command requires interactive input.'));
20
+ console.log(chalk.gray('Please provide a branch name:'));
21
+ console.log(chalk.gray(' gittable merge <branch-name>'));
22
+ process.exit(1);
23
+ }
24
+
25
+ const options = branches.local
26
+ .filter((branch) => !branch.current)
27
+ .map((branch) => ({
28
+ value: branch.name,
29
+ label: branch.name,
30
+ }));
31
+
32
+ if (options.length === 0) {
33
+ clack.cancel(chalk.yellow('No branches to merge'));
34
+ return;
35
+ }
36
+
37
+ branchToMerge = await clack.select({
38
+ message: chalk.cyan('Select branch to merge:'),
39
+ options,
40
+ });
41
+
42
+ if (clack.isCancel(branchToMerge)) {
43
+ clack.cancel(chalk.yellow('Cancelled'));
44
+ return;
45
+ }
46
+ }
47
+
48
+ const strategy = args.includes('--no-ff')
49
+ ? '--no-ff'
50
+ : args.includes('--squash')
51
+ ? '--squash'
52
+ : '';
53
+
54
+ const spinner = clack.spinner();
55
+ spinner.start(`Merging ${branchToMerge} into ${currentBranch}`);
56
+
57
+ const command = strategy ? `merge ${strategy} ${branchToMerge}` : `merge ${branchToMerge}`;
58
+ const result = execGit(command, { silent: false });
59
+ spinner.stop();
60
+
61
+ if (result.success) {
62
+ clack.outro(chalk.green.bold(`Merged ${branchToMerge} into ${currentBranch}`));
63
+ } else {
64
+ clack.cancel(chalk.red('Merge failed'));
65
+ console.error(result.error);
66
+ console.log(chalk.yellow('\nYou may need to resolve conflicts manually'));
67
+ process.exit(1);
68
+ }
69
+ };
package/commands/mv.js ADDED
@@ -0,0 +1,40 @@
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('MV');
8
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Move/Rename Files')}`);
9
+
10
+ const force = args.includes('--force') || args.includes('-f');
11
+ const files = args.filter((arg) => !arg.startsWith('--'));
12
+
13
+ if (files.length < 2) {
14
+ clack.cancel(chalk.yellow('Usage: mv <source> <destination>'));
15
+ return;
16
+ }
17
+
18
+ const source = files[0];
19
+ const destination = files[1];
20
+
21
+ const spinner = clack.spinner();
22
+ spinner.start(`Moving ${source} to ${destination}`);
23
+
24
+ let command = 'mv';
25
+ if (force) {
26
+ command += ' --force';
27
+ }
28
+ command += ` ${source} ${destination}`;
29
+
30
+ const result = execGit(command, { silent: false });
31
+ spinner.stop();
32
+
33
+ if (result.success) {
34
+ clack.outro(chalk.green.bold(`Moved ${source} to ${destination}`));
35
+ } else {
36
+ clack.cancel(chalk.red('Failed to move file'));
37
+ console.error(result.error);
38
+ process.exit(1);
39
+ }
40
+ };
@@ -0,0 +1,74 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const { execGit, getCurrentBranch, remoteExists, getRemotes } = require('../lib/git/exec');
4
+ const { showBanner } = require('../lib/ui/banner');
5
+ const { addRemote } = require('./remote');
6
+
7
+ module.exports = async (args) => {
8
+ showBanner('PULL');
9
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Pull from Remote')}`);
10
+
11
+ const branch = getCurrentBranch();
12
+ let remote = args[0] || 'origin';
13
+ let branchName = args[1] || branch;
14
+
15
+ // Handle empty repository (no commits = no branch)
16
+ if (!branchName || branchName === 'null' || branchName === 'HEAD') {
17
+ clack.cancel(chalk.red('No branch found'));
18
+ console.log(chalk.yellow('Repository has no commits yet.'));
19
+ console.log(chalk.gray('Make at least one commit before pulling.'));
20
+ process.exit(1);
21
+ }
22
+
23
+ if (!remoteExists(remote)) {
24
+ const remotes = getRemotes();
25
+ if (remotes.length === 0) {
26
+ console.log(chalk.yellow(`Remote '${remote}' does not exist.`));
27
+ const shouldAdd = await clack.confirm({
28
+ message: chalk.cyan(`Would you like to add remote '${remote}'?`),
29
+ initialValue: true,
30
+ });
31
+
32
+ if (clack.isCancel(shouldAdd) || !shouldAdd) {
33
+ clack.cancel(chalk.yellow('Cancelled'));
34
+ process.exit(1);
35
+ }
36
+
37
+ const added = await addRemote(remote, null);
38
+ if (!added) {
39
+ process.exit(1);
40
+ }
41
+ } else {
42
+ console.log(chalk.yellow(`Remote '${remote}' not found`));
43
+ console.log(chalk.dim(`Available remotes: ${remotes.join(', ')}`));
44
+ const shouldAdd = await clack.confirm({
45
+ message: chalk.cyan(`Would you like to add remote '${remote}'?`),
46
+ initialValue: true,
47
+ });
48
+
49
+ if (clack.isCancel(shouldAdd) || !shouldAdd) {
50
+ clack.cancel(chalk.yellow('Cancelled'));
51
+ process.exit(1);
52
+ }
53
+
54
+ const added = await addRemote(remote, null);
55
+ if (!added) {
56
+ process.exit(1);
57
+ }
58
+ }
59
+ }
60
+
61
+ const spinner = clack.spinner();
62
+ spinner.start(`Pulling from ${remote}/${branchName}`);
63
+
64
+ const result = execGit(`pull ${remote} ${branchName}`, { silent: false });
65
+ spinner.stop();
66
+
67
+ if (result.success) {
68
+ clack.outro(chalk.green.bold('Pull completed'));
69
+ } else {
70
+ clack.cancel(chalk.red('Pull failed'));
71
+ console.error(result.error);
72
+ process.exit(1);
73
+ }
74
+ };
@@ -0,0 +1,97 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const { execGit, getCurrentBranch, remoteExists, getRemotes } = require('../lib/git/exec');
4
+ const { showBanner } = require('../lib/ui/banner');
5
+ const { addRemote } = require('./remote');
6
+
7
+ module.exports = async (args) => {
8
+ showBanner('PUSH');
9
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Push to Remote')}`);
10
+
11
+ const branch = getCurrentBranch();
12
+ let remote = args[0] || 'origin';
13
+ let branchName = args[1] || branch;
14
+ const force = args.includes('--force') || args.includes('-f');
15
+
16
+ // Handle empty repository (no commits = no branch)
17
+ if (!branchName || branchName === 'null' || branchName === 'HEAD') {
18
+ clack.cancel(chalk.red('No branch found'));
19
+ console.log(chalk.yellow('Repository has no commits yet.'));
20
+ console.log(chalk.gray('Make at least one commit before pushing.'));
21
+ process.exit(1);
22
+ }
23
+
24
+ if (!remoteExists(remote)) {
25
+ const remotes = getRemotes();
26
+ if (remotes.length === 0) {
27
+ console.log(chalk.yellow(`Remote '${remote}' does not exist.`));
28
+ const shouldAdd = await clack.confirm({
29
+ message: chalk.cyan(`Would you like to add remote '${remote}'?`),
30
+ initialValue: true,
31
+ });
32
+
33
+ if (clack.isCancel(shouldAdd) || !shouldAdd) {
34
+ clack.cancel(chalk.yellow('Cancelled'));
35
+ process.exit(1);
36
+ }
37
+
38
+ const added = await addRemote(remote, null);
39
+ if (!added) {
40
+ process.exit(1);
41
+ }
42
+ } else {
43
+ console.log(chalk.yellow(`Remote '${remote}' not found`));
44
+ console.log(chalk.dim(`Available remotes: ${remotes.join(', ')}`));
45
+ const shouldAdd = await clack.confirm({
46
+ message: chalk.cyan(`Would you like to add remote '${remote}'?`),
47
+ initialValue: true,
48
+ });
49
+
50
+ if (clack.isCancel(shouldAdd) || !shouldAdd) {
51
+ clack.cancel(chalk.yellow('Cancelled'));
52
+ process.exit(1);
53
+ }
54
+
55
+ const added = await addRemote(remote, null);
56
+ if (!added) {
57
+ process.exit(1);
58
+ }
59
+ }
60
+ }
61
+
62
+ if (force) {
63
+ if (!process.stdin.isTTY) {
64
+ clack.cancel(chalk.red('Interactive mode required for force push'));
65
+ console.log(chalk.yellow('Force push requires confirmation.'));
66
+ console.log(
67
+ chalk.gray('Please use: gittable push <remote> <branch> --force (with confirmation)')
68
+ );
69
+ process.exit(1);
70
+ }
71
+
72
+ const confirm = await clack.confirm({
73
+ message: chalk.red('Force push? This can overwrite remote history.'),
74
+ initialValue: false,
75
+ });
76
+
77
+ if (clack.isCancel(confirm) || !confirm) {
78
+ clack.cancel(chalk.yellow('Cancelled'));
79
+ return;
80
+ }
81
+ }
82
+
83
+ const spinner = clack.spinner();
84
+ spinner.start(`Pushing to ${remote}/${branchName}`);
85
+
86
+ const command = force ? `push ${remote} ${branchName} --force` : `push ${remote} ${branchName}`;
87
+ const result = execGit(command, { silent: false });
88
+ spinner.stop();
89
+
90
+ if (result.success) {
91
+ clack.outro(chalk.green.bold('Push completed'));
92
+ } else {
93
+ clack.cancel(chalk.red('Push failed'));
94
+ console.error(result.error);
95
+ process.exit(1);
96
+ }
97
+ };