preva-cli 0.0.0-stage → 1.0.1
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 +21 -0
- package/README.md +171 -2
- package/bin/preva.js +10 -0
- package/package.json +40 -3
- package/src/commands/add.js +54 -0
- package/src/commands/commit.js +138 -0
- package/src/commands/push.js +98 -0
- package/src/index.js +83 -0
- package/src/utils/colors.js +27 -0
- package/src/utils/git.js +355 -0
- package/src/utils/scanner.js +142 -0
- package/src/utils/ui.js +352 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gozajpeg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,172 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/gozajpeg/preva/main/assets/preva.webp" alt="preva logo" width="380" />
|
|
3
|
+
<h1>preva</h1>
|
|
4
|
+
<p><strong>Same Git, just with a preview.</strong></p>
|
|
5
|
+
<p>Preva doesn't change how Git works. It just shows you what's happening before you stage, commit, or push, so you can catch mistakes before they leave your machine.</p>
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
<p>
|
|
8
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg?style=flat-square" alt="node version" /></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/preva-cli"><img src="https://img.shields.io/badge/npm-preva--cli-red.svg?style=flat-square" alt="npm package" /></a>
|
|
10
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license" /></a>
|
|
11
|
+
<a href="https://github.com/gozajpeg"><img src="https://img.shields.io/badge/author-gozajpeg-blue.svg?style=flat-square" alt="author" /></a>
|
|
12
|
+
</p>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### Overview
|
|
18
|
+
|
|
19
|
+
Preva doesn't change how Git works. It runs normal Git commands, but gives you a preview first so you don't push the wrong files by accident:
|
|
20
|
+
|
|
21
|
+
1. **Interactive Staging (`preva add`)**: Pick which files to stage from a checklist instead of running `git add .` blindly. Common secret files like `.env` start unchecked.
|
|
22
|
+
2. **Conventional Commits (`preva commit`)**: Pick standard commit types (`feat`, `fix`, `docs`, etc.) to keep your commit history clean and consistent.
|
|
23
|
+
3. **Pre-Push Review (`preva push`)**: See your unpushed commits and the exact files you're about to push, with an alert if any secret files got committed.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### Terminal Previews
|
|
28
|
+
|
|
29
|
+
#### 1. Interactive Staging (`preva git add .`)
|
|
30
|
+
|
|
31
|
+
Instead of staging everything blindly with `git add .`, run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
preva git add .
|
|
35
|
+
# or shorthand:
|
|
36
|
+
preva add .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`preva` scans your repository changes and displays an interactive checklist:
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
- **Normal changes** start pre-checked so common staging stays fast.
|
|
44
|
+
- **Sensitive files** (patterns like `*.key`, `*.pem`, `.env*`, `credentials.json`, `*.p12`) start **unchecked** by default.
|
|
45
|
+
- Use **arrow keys** or **j/k** to navigate, **Space** to toggle, **a** to toggle all, and **Enter** to confirm.
|
|
46
|
+
- Only the files you confirm will be staged with native `git add`.
|
|
47
|
+
|
|
48
|
+
You can also target specific folders or paths:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
preva git add src/
|
|
52
|
+
preva git add config/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
#### 2. Conventional Commit Generator (`preva git commit`)
|
|
58
|
+
|
|
59
|
+
Write clean, consistent Conventional Commits with interactive selectable types and pre-filled formatting:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
preva git commit
|
|
63
|
+
# or shorthand:
|
|
64
|
+
preva commit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`preva` reviews your staged changes, lets you select standard commit types, prompts for an optional scope, and previews the formatted message:
|
|
68
|
+
|
|
69
|
+

|
|
70
|
+
|
|
71
|
+
- **Standard Types**: Pre-fills the 11 standard types from Conventional Commits v1.0.0 and `@commitlint/config-conventional`.
|
|
72
|
+
- **Automatic Formatting**: Normalizes scopes to lowercase and auto-strips trailing periods so commits pass CI linters without friction.
|
|
73
|
+
- **Breaking Changes**: Automatically appends the standard exclamation mark `!` (e.g. `feat(api)!: remove v1 endpoint`) when a breaking change is confirmed.
|
|
74
|
+
- **Safety**: Warns if nothing is staged and suggests running `preva git add .` first.
|
|
75
|
+
- **Flag Pass-through**: Passes through native Git commit flags (e.g. `preva commit --amend` or `preva commit -m "custom message"`).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
#### 3. Pre-Push Review (`preva git push`)
|
|
80
|
+
|
|
81
|
+
Before pushing your branch, run:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
preva git push origin main
|
|
85
|
+
# or shorthand:
|
|
86
|
+
preva push
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`preva` inspects the unpushed commits and presents a clear summary:
|
|
90
|
+
|
|
91
|
+

|
|
92
|
+
|
|
93
|
+
- If flagged files are detected, the confirmation prompt defaults to **No (`[y/N]`)**.
|
|
94
|
+
- Pressing **n**, **Enter**, or **Ctrl+C** cleanly cancels the push without sending anything to the remote.
|
|
95
|
+
- When cancelled, `preva` prints the command to uncommit the file if you need to adjust it:
|
|
96
|
+
```bash
|
|
97
|
+
To uncommit and adjust before pushing:
|
|
98
|
+
git reset --soft HEAD~1
|
|
99
|
+
preva git add .
|
|
100
|
+
preva commit
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### What `preva` Flags
|
|
106
|
+
|
|
107
|
+
1. **Common Sensitive Patterns:** Identifies common credential and key formats by filename (`.env*`, `*.pem`, `*.key`, `id_rsa*`, `credentials.json`, `*.p12`, `*.keystore`, `*.p8`, `*.ovpn`, `.npmrc`, `.pypirc`).
|
|
108
|
+
2. **Tracked `.gitignore` Matches:** Flags files that match your project's `.gitignore` rules but are still being tracked because they were committed previously.
|
|
109
|
+
3. **Heavy Directories:** Highlights unignored directories such as `node_modules/` or `dist/` before they are staged.
|
|
110
|
+
4. **Renamed and Deleted Files:** Distinguishes renames (`R`) and deletions (`D`) in the checklist.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### Boundaries (What `preva` Is Not)
|
|
115
|
+
|
|
116
|
+
- **Not a Secret Scanner:** `preva` checks file names and paths against known patterns. It does not scan inside file contents for hardcoded tokens or API keys.
|
|
117
|
+
- **No Automatic History Alterations:** `preva` will never automatically rebase, reset, or amend commits for you.
|
|
118
|
+
- **No Native Git Interception:** It does not install Git hooks. Plain `git add` and `git push` will continue to run without `preva` unless explicitly invoked.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Installation & Usage
|
|
123
|
+
|
|
124
|
+
Install globally via npm:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm install -g preva-cli
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Or run directly with `npx`:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npx preva-cli git add .
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Commands
|
|
137
|
+
|
|
138
|
+
| Command | Description |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `preva git add .` | Review all changes in an interactive checklist before staging |
|
|
141
|
+
| `preva git add <paths...>` | Review specific files or folders before staging |
|
|
142
|
+
| `preva git commit` | Interactive Conventional Commit generator with selectable types |
|
|
143
|
+
| `preva git push [remote] [branch]` | Review unpushed commits and files before pushing |
|
|
144
|
+
| `preva --version` | Show installed version |
|
|
145
|
+
| `preva --help` | Show usage help and options |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### Non-Interactive Environments & CI
|
|
150
|
+
|
|
151
|
+
When run in environments without an interactive TTY (`!process.stdin.isTTY`):
|
|
152
|
+
- `preva` detects the missing terminal and exits cleanly with code `1` rather than hanging.
|
|
153
|
+
|
|
154
|
+
| Exit Code | Meaning |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `0` | Success: files staged or commits pushed |
|
|
157
|
+
| `1` | Error: not a git repo, no changes, or non-interactive terminal |
|
|
158
|
+
| `130` | Cancelled by user (`Ctrl+C` or chose `N`) |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### Design Philosophy
|
|
163
|
+
|
|
164
|
+
- **Zero External Dependencies:** Built entirely with Node.js standard library built-ins (`node:readline`, `node:child_process`, `node:fs`, `node:path`).
|
|
165
|
+
- **Zero Extra Config Files:** No `.prevaignore` dotfiles required. Works using built-in patterns and your project's `.gitignore`.
|
|
166
|
+
- **Defensive Execution:** Runs native Git commands using `execFile` with argument arrays and `--` path separators to avoid shell quoting and flag injection issues.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### License
|
|
171
|
+
|
|
172
|
+
MIT License © [gozajpeg](https://github.com/gozajpeg)
|
package/bin/preva.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "preva-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Same Git, just with a preview.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"preva": "bin/preva.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test test/**/*.test.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"git",
|
|
21
|
+
"git-add",
|
|
22
|
+
"git-push",
|
|
23
|
+
"preview",
|
|
24
|
+
"staging",
|
|
25
|
+
"guard",
|
|
26
|
+
"checklist",
|
|
27
|
+
"developer-tools",
|
|
28
|
+
"zero-dependency"
|
|
29
|
+
],
|
|
30
|
+
"author": "gozajpeg",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/gozajpeg/preva.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/gozajpeg/preva/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/gozajpeg/preva#readme",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
}
|
|
6
43
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isGitRepo, getChangedFiles, stageFiles, getRepoRoot } from '../utils/git.js';
|
|
2
|
+
import { scanFiles } from '../utils/scanner.js';
|
|
3
|
+
import { checkboxList } from '../utils/ui.js';
|
|
4
|
+
import { colors } from '../utils/colors.js';
|
|
5
|
+
|
|
6
|
+
export async function addCommand(paths = []) {
|
|
7
|
+
const isRepo = await isGitRepo();
|
|
8
|
+
if (!isRepo) {
|
|
9
|
+
console.error(colors.red('Error: Not a git repository (or any of the parent directories).'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const repoRoot = await getRepoRoot().catch(() => process.cwd());
|
|
14
|
+
const changed = await getChangedFiles(paths);
|
|
15
|
+
|
|
16
|
+
if (changed.length === 0) {
|
|
17
|
+
console.log(colors.dim('Nothing to stage (working tree clean).'));
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const scanned = scanFiles(changed, repoRoot);
|
|
22
|
+
|
|
23
|
+
const choices = scanned.map((file) => ({
|
|
24
|
+
name: file.path,
|
|
25
|
+
value: file.path,
|
|
26
|
+
checked: file.defaultChecked,
|
|
27
|
+
flagged: file.flagged,
|
|
28
|
+
reason: file.reason,
|
|
29
|
+
status: file.status,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const selected = await checkboxList({
|
|
33
|
+
message: 'Select files to stage:',
|
|
34
|
+
choices,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (selected.length === 0) {
|
|
38
|
+
console.log(colors.yellow('No files selected. Nothing staged.'));
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await stageFiles(selected);
|
|
44
|
+
const leftUnstaged = changed.length - selected.length;
|
|
45
|
+
const summaryMsg =
|
|
46
|
+
leftUnstaged > 0
|
|
47
|
+
? ` (${leftUnstaged} left unstaged)`
|
|
48
|
+
: '';
|
|
49
|
+
console.log(`Staged ${colors.bold(selected.length)} file(s)${summaryMsg}.`);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(colors.red(`Error staging files: ${err.message}`));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { isGitRepo, getStagedFiles, commitStaged } from '../utils/git.js';
|
|
2
|
+
import { selectPrompt, inputPrompt, confirmPrompt } from '../utils/ui.js';
|
|
3
|
+
import { colors } from '../utils/colors.js';
|
|
4
|
+
|
|
5
|
+
export const COMMIT_TYPES = [
|
|
6
|
+
{ value: 'feat', name: 'feat: A new feature' },
|
|
7
|
+
{ value: 'fix', name: 'fix: A bug fix' },
|
|
8
|
+
{ value: 'docs', name: 'docs: Documentation only changes' },
|
|
9
|
+
{ value: 'style', name: 'style: Changes that do not affect code logic (formatting, white-space)' },
|
|
10
|
+
{ value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature' },
|
|
11
|
+
{ value: 'perf', name: 'perf: A code change that improves performance' },
|
|
12
|
+
{ value: 'test', name: 'test: Adding missing tests or correcting existing tests' },
|
|
13
|
+
{ value: 'build', name: 'build: Changes affecting build system or external dependencies' },
|
|
14
|
+
{ value: 'ci', name: 'ci: Changes to CI configuration files and scripts' },
|
|
15
|
+
{ value: 'chore', name: 'chore: Other changes that do not modify src or test files' },
|
|
16
|
+
{ value: 'revert', name: 'revert: Reverts a previous commit' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format type, optional scope, and message into Conventional Commit standard.
|
|
21
|
+
* Adheres strictly to Conventional Commits v1.0.0 and @commitlint/config-conventional:
|
|
22
|
+
* - <type>(<scope>): <message>
|
|
23
|
+
* - lowercase scope
|
|
24
|
+
* - no trailing period
|
|
25
|
+
* - optional breaking change '!' indicator before colon
|
|
26
|
+
*/
|
|
27
|
+
export function formatCommitMessage(type, scope, message, isBreaking = false) {
|
|
28
|
+
const cleanScope = scope ? scope.trim().toLowerCase() : '';
|
|
29
|
+
let cleanMsg = message ? message.trim() : '';
|
|
30
|
+
if (cleanMsg.endsWith('.')) {
|
|
31
|
+
cleanMsg = cleanMsg.slice(0, -1).trim();
|
|
32
|
+
}
|
|
33
|
+
const bang = isBreaking ? '!' : '';
|
|
34
|
+
if (cleanScope) {
|
|
35
|
+
return `${type}(${cleanScope})${bang}: ${cleanMsg}`;
|
|
36
|
+
}
|
|
37
|
+
return `${type}${bang}: ${cleanMsg}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function commitCommand(args = []) {
|
|
41
|
+
const isRepo = await isGitRepo();
|
|
42
|
+
if (!isRepo) {
|
|
43
|
+
console.error(colors.red('Error: Not a git repository (or any of the parent directories).'));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If user already passed a commit message via -m or --message, execute git commit directly
|
|
48
|
+
const hasMsgFlag = args.includes('-m') || args.includes('--message');
|
|
49
|
+
if (hasMsgFlag) {
|
|
50
|
+
try {
|
|
51
|
+
const { stdout } = await commitStaged('', args);
|
|
52
|
+
console.log(stdout.trim());
|
|
53
|
+
return;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(colors.red(`Commit failed: ${err.message}`));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check if staged changes exist
|
|
61
|
+
const staged = await getStagedFiles();
|
|
62
|
+
const hasOverrideFlags =
|
|
63
|
+
args.includes('-a') ||
|
|
64
|
+
args.includes('--all') ||
|
|
65
|
+
args.includes('--allow-empty') ||
|
|
66
|
+
args.includes('--amend');
|
|
67
|
+
|
|
68
|
+
if (staged.length === 0 && !hasOverrideFlags) {
|
|
69
|
+
console.log(colors.yellow('No staged changes to commit.'));
|
|
70
|
+
console.log(`Stage changes first using: ${colors.cyan('preva git add .')}\n`);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Show staged files summary
|
|
75
|
+
if (staged.length > 0) {
|
|
76
|
+
console.log(`\n${colors.bold('Staged changes')} (${colors.cyan(staged.length)}):`);
|
|
77
|
+
staged.slice(0, 10).forEach((f) => {
|
|
78
|
+
let badge = colors.cyan('M');
|
|
79
|
+
if (f.status === 'new') badge = colors.green('A');
|
|
80
|
+
else if (f.status === 'deleted') badge = colors.red('D');
|
|
81
|
+
else if (f.status === 'renamed') badge = colors.yellow('R');
|
|
82
|
+
console.log(` • ${badge} ${f.path}`);
|
|
83
|
+
});
|
|
84
|
+
if (staged.length > 10) {
|
|
85
|
+
console.log(colors.dim(` ... and ${staged.length - 10} more files`));
|
|
86
|
+
}
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 1. Select Conventional Commit Type
|
|
91
|
+
const type = await selectPrompt({
|
|
92
|
+
message: 'Select commit type:',
|
|
93
|
+
choices: COMMIT_TYPES,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// 2. Input Scope (optional)
|
|
97
|
+
const scope = await inputPrompt({
|
|
98
|
+
message: 'Scope (optional, e.g. auth, api, ui)',
|
|
99
|
+
placeholder: 'press enter to skip',
|
|
100
|
+
allowEmpty: true,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 3. Input Commit Message Description
|
|
104
|
+
const message = await inputPrompt({
|
|
105
|
+
message: 'Commit message',
|
|
106
|
+
placeholder: 'imperative, e.g. add user login',
|
|
107
|
+
allowEmpty: false,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 4. Breaking change check
|
|
111
|
+
const isBreaking = await confirmPrompt({
|
|
112
|
+
message: 'Is this a breaking change?',
|
|
113
|
+
defaultValue: false,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const fullMessage = formatCommitMessage(type, scope, message, isBreaking);
|
|
117
|
+
|
|
118
|
+
// 5. Preview and Confirm
|
|
119
|
+
console.log(`\n${colors.bold('Commit preview:')} ${colors.green(fullMessage)}`);
|
|
120
|
+
|
|
121
|
+
const proceed = await confirmPrompt({
|
|
122
|
+
message: 'Create this commit?',
|
|
123
|
+
defaultValue: true,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!proceed) {
|
|
127
|
+
console.log(colors.dim('\nCommit cancelled. Nothing committed.'));
|
|
128
|
+
process.exit(130);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const { stdout } = await commitStaged(fullMessage, args);
|
|
133
|
+
console.log(`\n${stdout.trim()}`);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(colors.red(`Commit failed: ${err.message}`));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { isGitRepo, getUnpushedCommits, getUnpushedFiles, pushCommits } from '../utils/git.js';
|
|
2
|
+
import { scanFiles } from '../utils/scanner.js';
|
|
3
|
+
import { confirmPrompt } from '../utils/ui.js';
|
|
4
|
+
import { colors } from '../utils/colors.js';
|
|
5
|
+
|
|
6
|
+
export async function pushCommand(args = []) {
|
|
7
|
+
const isRepo = await isGitRepo();
|
|
8
|
+
if (!isRepo) {
|
|
9
|
+
console.error(colors.red('Error: Not a git repository (or any of the parent directories).'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Parse remote and branch if provided (ignoring flags like -u, --force, etc.)
|
|
14
|
+
const nonFlagArgs = args.filter((a) => !a.startsWith('-'));
|
|
15
|
+
const remote = nonFlagArgs[0] || null;
|
|
16
|
+
const branch = nonFlagArgs[1] || null;
|
|
17
|
+
|
|
18
|
+
const { commits, upstreamRef, noCommits } = await getUnpushedCommits(process.cwd(), remote, branch);
|
|
19
|
+
|
|
20
|
+
if (noCommits) {
|
|
21
|
+
console.log(colors.yellow('No commits on current branch yet. Create a commit first with git commit.'));
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (commits.length === 0) {
|
|
26
|
+
console.log(colors.dim('Everything up-to-date. No commits to push.'));
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const unpushedFiles = await getUnpushedFiles(process.cwd(), upstreamRef, commits);
|
|
31
|
+
const scannedFiles = scanFiles(unpushedFiles);
|
|
32
|
+
const flaggedFiles = scannedFiles.filter((f) => f.flagged);
|
|
33
|
+
|
|
34
|
+
// 1. Commits preview
|
|
35
|
+
console.log(`\n${colors.bold('Unpushed Commits')} (${colors.cyan(commits.length)}):`);
|
|
36
|
+
commits.slice(0, 10).forEach((c) => {
|
|
37
|
+
console.log(` • ${colors.yellow(c.hash)} ${c.message} ${colors.dim(`(${c.author})`)}`);
|
|
38
|
+
});
|
|
39
|
+
if (commits.length > 10) {
|
|
40
|
+
console.log(colors.dim(` ... and ${commits.length - 10} more commits`));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Files preview
|
|
44
|
+
console.log(`\n${colors.bold('Files leaving your machine')} (${colors.cyan(scannedFiles.length)}):`);
|
|
45
|
+
scannedFiles.slice(0, 20).forEach((f) => {
|
|
46
|
+
let statusBadge = colors.cyan('M');
|
|
47
|
+
if (f.status === 'new') statusBadge = colors.green('A');
|
|
48
|
+
else if (f.status === 'deleted') statusBadge = colors.red('D');
|
|
49
|
+
else if (f.status === 'renamed') statusBadge = colors.yellow('R');
|
|
50
|
+
|
|
51
|
+
let flagBadge = '';
|
|
52
|
+
if (f.flagged) {
|
|
53
|
+
flagBadge = colors.yellow(` [FLAGGED: ${f.reason || 'sensitive'}]`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(` • ${statusBadge} ${f.path}${flagBadge}`);
|
|
57
|
+
});
|
|
58
|
+
if (scannedFiles.length > 20) {
|
|
59
|
+
console.log(colors.dim(` ... and ${scannedFiles.length - 20} more files`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('');
|
|
63
|
+
|
|
64
|
+
// 3. Flagged warning
|
|
65
|
+
if (flaggedFiles.length > 0) {
|
|
66
|
+
console.log(
|
|
67
|
+
colors.yellow(
|
|
68
|
+
`Notice: ${colors.bold(flaggedFiles.length)} flagged file(s) detected in unpushed commits.`
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const targetName = remote && branch ? `${remote}/${branch}` : upstreamRef || 'remote';
|
|
74
|
+
const shouldDefaultNo = flaggedFiles.length > 0;
|
|
75
|
+
|
|
76
|
+
const proceed = await confirmPrompt({
|
|
77
|
+
message: `Push ${commits.length} commit(s) to ${targetName}?`,
|
|
78
|
+
defaultValue: !shouldDefaultNo,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!proceed) {
|
|
82
|
+
console.log(colors.dim('\nPush cancelled. No commits or files were pushed.'));
|
|
83
|
+
if (flaggedFiles.length > 0) {
|
|
84
|
+
console.log(`\n${colors.bold('To uncommit and adjust before pushing:')}`);
|
|
85
|
+
console.log(` ${colors.cyan('git reset --soft HEAD~1')}`);
|
|
86
|
+
console.log(` ${colors.cyan('preva git add .')}`);
|
|
87
|
+
console.log(` ${colors.cyan('git commit -m "your message"')}\n`);
|
|
88
|
+
}
|
|
89
|
+
process.exit(130);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await pushCommits(args);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(colors.red(`Push failed: ${err.message}`));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { addCommand } from './commands/add.js';
|
|
3
|
+
import { commitCommand } from './commands/commit.js';
|
|
4
|
+
import { pushCommand } from './commands/push.js';
|
|
5
|
+
import { colors } from './utils/colors.js';
|
|
6
|
+
|
|
7
|
+
function getVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
10
|
+
return pkg.version;
|
|
11
|
+
} catch {
|
|
12
|
+
return '0.1.0';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function printHelp() {
|
|
17
|
+
console.log(`
|
|
18
|
+
${colors.bold('preva')} - Interactive Git Staging, Commit & Push Preview Guard
|
|
19
|
+
|
|
20
|
+
${colors.bold('USAGE:')}
|
|
21
|
+
${colors.cyan('preva git add')} [paths...] Review and select files before staging
|
|
22
|
+
${colors.cyan('preva git commit')} [args...] Interactive Conventional Commit generator
|
|
23
|
+
${colors.cyan('preva git push')} [remote] [branch] Preview commits and files before pushing
|
|
24
|
+
${colors.cyan('preva')} --version Show version
|
|
25
|
+
${colors.cyan('preva')} --help Show this help message
|
|
26
|
+
|
|
27
|
+
${colors.bold('EXAMPLES:')}
|
|
28
|
+
$ preva git add .
|
|
29
|
+
$ preva git commit
|
|
30
|
+
$ preva git push origin main
|
|
31
|
+
|
|
32
|
+
${colors.dim('Zero external dependencies. Pure Node.js.')}
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
37
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
38
|
+
printHelp();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
43
|
+
console.log(getVersion());
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [first, second, ...rest] = argv;
|
|
48
|
+
|
|
49
|
+
// Handle "git add" or shorthand "add"
|
|
50
|
+
if (first === 'git' && second === 'add') {
|
|
51
|
+
await addCommand(rest);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (first === 'add') {
|
|
55
|
+
await addCommand([second, ...rest].filter(Boolean));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Handle "git commit" or shorthand "commit"
|
|
60
|
+
if (first === 'git' && second === 'commit') {
|
|
61
|
+
await commitCommand(rest);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (first === 'commit') {
|
|
65
|
+
await commitCommand([second, ...rest].filter(Boolean));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle "git push" or shorthand "push"
|
|
70
|
+
if (first === 'git' && second === 'push') {
|
|
71
|
+
await pushCommand(rest);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (first === 'push') {
|
|
75
|
+
await pushCommand([second, ...rest].filter(Boolean));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.error(
|
|
80
|
+
colors.red(`Unknown command: ${argv.join(' ')}\nRun "${colors.cyan('preva --help')}" for usage instructions.`)
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Zero-dependency terminal colors supporting NO_COLOR and TTY detection
|
|
2
|
+
|
|
3
|
+
const isColorSupported =
|
|
4
|
+
!process.env.NO_COLOR &&
|
|
5
|
+
(process.stdout.isTTY || process.env.FORCE_COLOR !== undefined);
|
|
6
|
+
|
|
7
|
+
function style(start, end) {
|
|
8
|
+
return (str) => (isColorSupported ? `\x1b[${start}m${str}\x1b[${end}m` : String(str));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const colors = {
|
|
12
|
+
reset: isColorSupported ? '\x1b[0m' : '',
|
|
13
|
+
bold: style(1, 22),
|
|
14
|
+
dim: style(2, 22),
|
|
15
|
+
italic: style(3, 23),
|
|
16
|
+
underline: style(4, 24),
|
|
17
|
+
red: style(31, 39),
|
|
18
|
+
green: style(32, 39),
|
|
19
|
+
yellow: style(33, 39),
|
|
20
|
+
blue: style(34, 39),
|
|
21
|
+
magenta: style(35, 39),
|
|
22
|
+
cyan: style(36, 39),
|
|
23
|
+
white: style(37, 39),
|
|
24
|
+
gray: style(90, 39),
|
|
25
|
+
bgRed: style(41, 49),
|
|
26
|
+
bgYellow: style(43, 49),
|
|
27
|
+
};
|