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,57 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const { isGitRepo } = require('../lib/git/exec');
4
+ const { showBanner } = require('../lib/ui/banner');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+
8
+ module.exports = async (args) => {
9
+ showBanner('UNINIT');
10
+ console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Remove Git Repository')}`);
11
+
12
+ if (!isGitRepo()) {
13
+ clack.cancel(chalk.yellow('Not a git repository'));
14
+ return;
15
+ }
16
+
17
+ const force = args.includes('--force') || args.includes('-f');
18
+ const gitDir = path.join(process.cwd(), '.git');
19
+
20
+ if (!force) {
21
+ const confirm = await clack.confirm({
22
+ message: chalk.red('This will permanently delete all git history. Continue?'),
23
+ initialValue: false,
24
+ });
25
+
26
+ if (clack.isCancel(confirm) || !confirm) {
27
+ clack.cancel(chalk.yellow('Cancelled'));
28
+ return;
29
+ }
30
+ }
31
+
32
+ const spinner = clack.spinner();
33
+ spinner.start('Removing git repository');
34
+
35
+ try {
36
+ // Check if .git is a file (submodule) or directory
37
+ const gitStat = fs.statSync(gitDir);
38
+
39
+ if (gitStat.isFile()) {
40
+ // It's a submodule - just remove the file
41
+ fs.unlinkSync(gitDir);
42
+ } else if (gitStat.isDirectory()) {
43
+ // Remove the entire .git directory
44
+ fs.rmSync(gitDir, { recursive: true, force: true });
45
+ }
46
+
47
+ spinner.stop();
48
+ clack.outro(
49
+ chalk.green.bold('Git repository removed. You can now run "gittable init" for a fresh start.')
50
+ );
51
+ } catch (error) {
52
+ spinner.stop();
53
+ clack.cancel(chalk.red('Failed to remove git repository'));
54
+ console.error(error.message);
55
+ process.exit(1);
56
+ }
57
+ };
package/index.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ declare module 'gittable' {
2
+ export interface CommitType {
3
+ value: string;
4
+ name: string;
5
+ }
6
+
7
+ export interface Scope {
8
+ name: string;
9
+ }
10
+
11
+ export interface Config {
12
+ types: CommitType[];
13
+ scopes?: Scope[];
14
+ scopeOverrides?: Record<string, Scope[]>;
15
+
16
+ allowTicketNumber?: boolean;
17
+ isTicketNumberRequired?: boolean;
18
+ ticketNumberPrefix?: string;
19
+ ticketNumberSuffix?: string;
20
+ ticketNumberRegExp?: string;
21
+ fallbackTicketNumber?: string;
22
+ prependTicketToHead?: boolean;
23
+
24
+ allowCustomScopes?: boolean;
25
+ allowBreakingChanges?: string[];
26
+ skipQuestions?: string[];
27
+ skipEmptyScopes?: boolean;
28
+
29
+ subjectLimit?: number;
30
+ subjectSeparator?: string;
31
+ typePrefix?: string;
32
+ typeSuffix?: string;
33
+ upperCaseSubject?: boolean;
34
+
35
+ breaklineChar?: string;
36
+ breakingPrefix?: string;
37
+ footerPrefix?: string;
38
+
39
+ usePreparedCommit?: boolean;
40
+ askForBreakingChangeFirst?: boolean;
41
+ }
42
+
43
+ export interface Answers {
44
+ type: string;
45
+ scope: string | null;
46
+ ticketNumber?: string;
47
+ subject: string;
48
+ body?: string;
49
+ breaking?: string;
50
+ footer?: string;
51
+ }
52
+
53
+ export type Prompter = (cz: unknown, commit: (message: string) => void) => Promise<void>;
54
+
55
+ export const prompter: Prompter;
56
+ }
package/index.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ const clack = require('@clack/prompts');
4
+ const chalk = require('chalk');
5
+ const { promptQuestions } = require('./lib/commit/questions');
6
+ const buildCommit = require('./lib/commit/build-commit');
7
+ const readConfigFile = require('./lib/config/read-config-file');
8
+
9
+ const prompter = async (_, commit) => {
10
+ const config = readConfigFile();
11
+ if (!config) process.exit(1);
12
+
13
+ config.subjectLimit = config.subjectLimit || 100;
14
+
15
+ try {
16
+ const answers = await promptQuestions(config);
17
+ const message = buildCommit(answers, config);
18
+
19
+ clack.note(message, chalk.bold('Commit Preview'));
20
+
21
+ const action = await clack.select({
22
+ message: chalk.yellow('Proceed?'),
23
+ options: [
24
+ { value: 'yes', label: chalk.green('Commit') },
25
+ { value: 'no', label: chalk.red('Cancel') },
26
+ ],
27
+ });
28
+
29
+ if (clack.isCancel(action) || action === 'no') {
30
+ clack.cancel(chalk.yellow('Cancelled'));
31
+ process.exit(0);
32
+ }
33
+
34
+ const spinner = clack.spinner();
35
+ spinner.start('Creating commit');
36
+
37
+ await new Promise((resolve, reject) => {
38
+ try {
39
+ commit(message);
40
+ resolve();
41
+ } catch (err) {
42
+ reject(err);
43
+ }
44
+ });
45
+
46
+ spinner.stop('Commit created');
47
+ clack.outro(chalk.green.bold('Success'));
48
+ } catch (error) {
49
+ clack.cancel(chalk.red('Failed'));
50
+ console.error(error);
51
+ process.exit(1);
52
+ }
53
+ };
54
+
55
+ module.exports = { prompter };
@@ -0,0 +1,64 @@
1
+ const wrap = require('word-wrap');
2
+
3
+ const DEFAULTS = {
4
+ separator: ': ',
5
+ maxWidth: 100,
6
+ breakChar: '|',
7
+ };
8
+
9
+ const wrapText = (text, width = DEFAULTS.maxWidth) =>
10
+ text ? wrap(text, { trim: true, newline: '\n', indent: '', width }) : '';
11
+
12
+ const escapeChars = (str) => str.replace(/[`"\\$!<>&]/g, (char) => `\\${char}`);
13
+
14
+ const addBreaklines = (text, char = DEFAULTS.breakChar) => text.split(char).join('\n');
15
+
16
+ const buildCommit = (answers, config) => {
17
+ const parts = [];
18
+
19
+ // Build header: type(scope): subject
20
+ let header = config.typePrefix || '';
21
+ header += answers.type;
22
+ header += config.typeSuffix || '';
23
+
24
+ if (answers.scope) {
25
+ header += `(${answers.scope})`;
26
+ }
27
+
28
+ header += config.subjectSeparator || DEFAULTS.separator;
29
+
30
+ if (config.prependTicketToHead && answers.ticketNumber) {
31
+ const ticket = answers.ticketNumber.trim();
32
+ const prefix = config.ticketNumberPrefix || '';
33
+ const suffix = config.ticketNumberSuffix || '';
34
+ header = `${prefix}${ticket}${suffix} ${header}`;
35
+ }
36
+
37
+ header += answers.subject.slice(0, config.subjectLimit || 100);
38
+
39
+ parts.push(header);
40
+
41
+ // Add body
42
+ if (answers.body) {
43
+ const body = wrapText(addBreaklines(answers.body, config.breaklineChar));
44
+ parts.push('', body);
45
+ }
46
+
47
+ // Add breaking changes
48
+ if (answers.breaking) {
49
+ const prefix = config.breakingPrefix || 'BREAKING CHANGE:';
50
+ const breaking = wrapText(answers.breaking);
51
+ parts.push('', `${prefix}\n${breaking}`);
52
+ }
53
+
54
+ // Add footer
55
+ if (answers.footer) {
56
+ const prefix = config.footerPrefix === '' ? '' : config.footerPrefix || 'ISSUES CLOSED:';
57
+ const footer = wrapText(addBreaklines(answers.footer, config.breaklineChar));
58
+ parts.push('', prefix ? `${prefix} ${footer}` : footer);
59
+ }
60
+
61
+ return escapeChars(parts.join('\n'));
62
+ };
63
+
64
+ module.exports = buildCommit;
@@ -0,0 +1,15 @@
1
+ const fs = require('node:fs');
2
+
3
+ const getPreviousCommit = () => {
4
+ const path = './.git/COMMIT_EDITMSG';
5
+ if (!fs.existsSync(path)) return null;
6
+
7
+ return fs
8
+ .readFileSync(path, 'utf-8')
9
+ .replace(/^#.*/gm, '')
10
+ .replace(/^\s*[\r\n]/gm, '')
11
+ .replace(/[\r\n]$/, '')
12
+ .split(/\r\n|\r|\n/);
13
+ };
14
+
15
+ module.exports = getPreviousCommit;
@@ -0,0 +1,226 @@
1
+ const clack = require('@clack/prompts');
2
+ const chalk = require('chalk');
3
+ const _fs = require('node:fs');
4
+ const getPreviousCommit = require('./get-previous-commit');
5
+
6
+ // Constants
7
+ const SCOPE_CATEGORIES = {
8
+ 'Core App Structure': ['app', 'routing', 'layout', 'middleware'],
9
+
10
+ 'Rendering & Runtime': ['rsc', 'client', 'server'],
11
+
12
+ 'UI & Shared Logic': ['components', 'styles', 'hooks', 'utils', 'types'],
13
+
14
+ 'Data & API': ['api', 'auth', 'db', 'cache'],
15
+
16
+ 'Dev & Tooling': ['config', 'logging', 'test'],
17
+
18
+ Infrastructure: ['infra', 'env'],
19
+ };
20
+
21
+ const categorizeScopesOptimized = (scopes) => {
22
+ const result = {};
23
+ const uncategorized = [];
24
+
25
+ // Initialize categories
26
+ for (const cat of Object.keys(SCOPE_CATEGORIES)) {
27
+ result[cat] = [];
28
+ }
29
+
30
+ // Build lookup map for O(1) categorization
31
+ const lookup = new Map();
32
+ for (const [cat, scopeNames] of Object.entries(SCOPE_CATEGORIES)) {
33
+ for (const name of scopeNames) {
34
+ lookup.set(name, cat);
35
+ }
36
+ }
37
+
38
+ // Categorize scopes
39
+ for (const scope of scopes) {
40
+ const name = typeof scope === 'string' ? scope : scope.name;
41
+ const category = lookup.get(name);
42
+
43
+ if (category) {
44
+ result[category].push(scope);
45
+ } else {
46
+ uncategorized.push(scope);
47
+ }
48
+ }
49
+
50
+ if (uncategorized.length > 0) {
51
+ result.Other = uncategorized;
52
+ }
53
+
54
+ // Remove empty categories
55
+ return Object.fromEntries(Object.entries(result).filter(([_, scopes]) => scopes.length > 0));
56
+ };
57
+
58
+ const handleCancel = () => {
59
+ clack.cancel(chalk.yellow('Operation cancelled'));
60
+ process.exit(0);
61
+ };
62
+
63
+ const createValidator = (config, field) => {
64
+ const validators = {
65
+ ticket: (value) => {
66
+ if (!value && config.isTicketNumberRequired && !config.fallbackTicketNumber) {
67
+ return 'Ticket number is required';
68
+ }
69
+ if (value && config.ticketNumberRegExp) {
70
+ const regex = new RegExp(config.ticketNumberRegExp);
71
+ if (!regex.test(value)) {
72
+ return `Must match pattern: ${config.ticketNumberRegExp}`;
73
+ }
74
+ }
75
+ },
76
+ subject: (value) => {
77
+ if (!value) return 'Subject is required';
78
+ const limit = config.subjectLimit || 100;
79
+ if (value.length > limit) {
80
+ return chalk.red(`Exceeds ${limit} chars (current: ${value.length})`);
81
+ }
82
+ },
83
+ };
84
+
85
+ return validators[field];
86
+ };
87
+
88
+ // Main prompt function
89
+ async function promptQuestions(config) {
90
+ // Optimize scope handling
91
+ const scopes = config.scopeOverrides || config.scopes || [];
92
+ const categorizedScopes = scopes.length ? categorizeScopesOptimized(scopes) : null;
93
+
94
+ // Use clack.group for better performance and UX
95
+ const answers = await clack.group(
96
+ {
97
+ type: () =>
98
+ clack.select({
99
+ message: chalk.cyan('Select commit type:'),
100
+ options: config.types.map((t) => ({
101
+ value: t.value,
102
+ label: t.name,
103
+ hint: t.value,
104
+ })),
105
+ }),
106
+
107
+ scopeCategory: ({ results }) => {
108
+ if (!categorizedScopes || results.type === 'wip') return Promise.resolve(null);
109
+
110
+ const categories = Object.keys(categorizedScopes);
111
+ const options = categories.map((cat) => ({
112
+ value: cat,
113
+ label: cat,
114
+ hint: `${categorizedScopes[cat].length} scopes`,
115
+ }));
116
+
117
+ options.push(
118
+ { value: '__empty__', label: chalk.dim('No scope') },
119
+ { value: '__custom__', label: chalk.dim('Custom scope') }
120
+ );
121
+
122
+ return clack.select({
123
+ message: chalk.cyan('Select scope category:'),
124
+ options,
125
+ });
126
+ },
127
+
128
+ scope: ({ results }) => {
129
+ if (!results.scopeCategory || results.scopeCategory === '__empty__') {
130
+ return Promise.resolve(null);
131
+ }
132
+
133
+ if (results.scopeCategory === '__custom__') {
134
+ return clack.text({
135
+ message: chalk.cyan('Enter custom scope:'),
136
+ placeholder: 'e.g., auth, api, ui',
137
+ });
138
+ }
139
+
140
+ const categoryScopes = categorizedScopes[results.scopeCategory];
141
+ return clack.select({
142
+ message: chalk.cyan('Select scope:'),
143
+ options: categoryScopes.map((s) => {
144
+ const name = typeof s === 'string' ? s : s.name;
145
+ return { value: name, label: name };
146
+ }),
147
+ });
148
+ },
149
+
150
+ ticketNumber: () => {
151
+ if (!config.allowTicketNumber) return Promise.resolve('');
152
+
153
+ return clack.text({
154
+ message: chalk.cyan('Ticket number:'),
155
+ placeholder: config.ticketNumberPrefix || 'TICKET-',
156
+ defaultValue: config.fallbackTicketNumber || '',
157
+ validate: createValidator(config, 'ticket'),
158
+ });
159
+ },
160
+
161
+ subject: () => {
162
+ const previous = getPreviousCommit();
163
+ const defaultValue = (config.usePreparedCommit && previous?.[0]) || '';
164
+
165
+ return clack.text({
166
+ message: chalk.cyan('Commit message:'),
167
+ placeholder: 'add user authentication',
168
+ defaultValue,
169
+ validate: createValidator(config, 'subject'),
170
+ });
171
+ },
172
+
173
+ body: () => {
174
+ if (config.skipQuestions?.includes('body')) return Promise.resolve('');
175
+
176
+ const previous = getPreviousCommit();
177
+ const defaultValue =
178
+ config.usePreparedCommit && previous?.length > 1 ? previous.slice(1).join('|') : '';
179
+
180
+ return clack.text({
181
+ message: chalk.cyan('Extended description (optional):'),
182
+ placeholder: 'Use "|" for new lines',
183
+ defaultValue,
184
+ });
185
+ },
186
+
187
+ breaking: ({ results }) => {
188
+ const shouldAsk =
189
+ config.askForBreakingChangeFirst || config.allowBreakingChanges?.includes(results.type);
190
+
191
+ if (!shouldAsk) return Promise.resolve('');
192
+
193
+ return clack.text({
194
+ message: chalk.red('Breaking changes (optional):'),
195
+ placeholder: 'Describe breaking changes',
196
+ });
197
+ },
198
+
199
+ footer: ({ results }) => {
200
+ if (results.type === 'wip' || config.skipQuestions?.includes('footer')) {
201
+ return Promise.resolve('');
202
+ }
203
+
204
+ return clack.text({
205
+ message: chalk.cyan('Issues closed (optional):'),
206
+ placeholder: '#31, #34',
207
+ });
208
+ },
209
+ },
210
+ {
211
+ onCancel: handleCancel,
212
+ }
213
+ );
214
+
215
+ // Process subject casing
216
+ if (answers.subject) {
217
+ const shouldUpperCase = config.upperCaseSubject || false;
218
+ answers.subject = shouldUpperCase
219
+ ? answers.subject.charAt(0).toUpperCase() + answers.subject.slice(1)
220
+ : answers.subject.charAt(0).toLowerCase() + answers.subject.slice(1);
221
+ }
222
+
223
+ return answers;
224
+ }
225
+
226
+ module.exports = { promptQuestions };
@@ -0,0 +1,54 @@
1
+ const findConfig = require('find-config');
2
+ const path = require('node:path');
3
+ const chalk = require('chalk');
4
+ const log = require('../utils/logger');
5
+
6
+ const CONFIG_NAMES = ['.gittable.js', '.gittable.json'];
7
+
8
+ const showError = () => {
9
+ console.log();
10
+ log.error(chalk.red.bold('No configuration found!'));
11
+ console.log();
12
+ console.log(chalk.yellow(' Create one of:'));
13
+ for (const name of CONFIG_NAMES) {
14
+ console.log(chalk.cyan(` • ${name}`));
15
+ }
16
+ console.log(chalk.cyan(' • package.json config'));
17
+ console.log();
18
+ console.log(chalk.gray(' Docs: github.com/leonardoanalista/cz-customizable'));
19
+ console.log();
20
+ };
21
+
22
+ const readConfigFile = () => {
23
+ // Try .cz-config.js
24
+ const jsConfig = findConfig.require(CONFIG_NAMES[0], { home: false });
25
+ if (jsConfig) {
26
+ log.success(chalk.green(`Using: ${chalk.bold(CONFIG_NAMES[0])}`));
27
+ return jsConfig;
28
+ }
29
+
30
+ // Try .cz-config.json
31
+ const jsonConfig = findConfig.require(CONFIG_NAMES[1], { home: false });
32
+ if (jsonConfig) {
33
+ log.success(chalk.green(`Using: ${chalk.bold(CONFIG_NAMES[1])}`));
34
+ return jsonConfig;
35
+ }
36
+
37
+ // Try package.json
38
+ const pkgPath = findConfig('package.json', { home: false });
39
+ if (pkgPath) {
40
+ const pkg = require(pkgPath);
41
+ const configPath = pkg.config?.['cz-customizable']?.config;
42
+
43
+ if (configPath) {
44
+ const fullPath = path.resolve(path.dirname(pkgPath), configPath);
45
+ log.success(chalk.green(`Using: ${chalk.bold(fullPath)}`));
46
+ return require(fullPath);
47
+ }
48
+ }
49
+
50
+ showError();
51
+ return null;
52
+ };
53
+
54
+ module.exports = readConfigFile;