codestamp 0.1.9__tar.gz → 0.2.0__tar.gz

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.
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: codestamp
3
+ Version: 0.2.0
4
+ Summary: CLI tool to manage copyright headers in source-code files
5
+ Author: Shardul Kulkarni
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Medhavee-Inc/codestamp
8
+ Project-URL: Issues, https://github.com/Medhavee-Inc/codestamp/issues
9
+ Keywords: copyright,license,header,cli,source-code
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Version Control
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: click>=8.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Requires-Dist: pytest-cov; extra == "dev"
26
+ Requires-Dist: build; extra == "dev"
27
+ Requires-Dist: twine; extra == "dev"
28
+
29
+ # codestamp
30
+
31
+ > Stamp and manage copyright headers across your source-code files.
32
+
33
+ [![PyPI](https://img.shields.io/pypi/v/codestamp)](https://pypi.org/project/codestamp/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/codestamp)](https://pypi.org/project/codestamp/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
36
+
37
+ Built by **Medhavee Inc** · Author **Shardul Kulkarni**
38
+
39
+ ---
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install codestamp
45
+ ```
46
+
47
+ Or for local development:
48
+
49
+ ```bash
50
+ git clone https://github.com/yourusername/codestamp
51
+ cd codestamp
52
+ pip install -r requirements-dev.txt
53
+ pip install -e .
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Quick start
59
+
60
+ 1. Create a `license.txt` in your project root whose **first line** is your copyright statement:
61
+
62
+ ```
63
+ Copyright (c) 2026 Medhavee Inc. All rights reserved. Proprietary and confidential.
64
+ ```
65
+
66
+ 2. Run from the project root:
67
+
68
+ ```bash
69
+ # Add headers to files changed in the last git commit (CI-friendly default)
70
+ codestamp
71
+
72
+ # Add headers to ALL source files in the tree
73
+ codestamp --bulk
74
+
75
+ # Add headers to staged files only
76
+ codestamp --staged
77
+
78
+ # Skip specific files or directories using an exclusion file
79
+ codestamp --bulk --exclude .csignore
80
+
81
+ # Preview without writing anything
82
+ codestamp --bulk --dry-run
83
+
84
+ # Update every file's copyright to a new statement
85
+ codestamp update "Copyright (c) 2026 Medhavee Foundation. All rights reserved."
86
+
87
+ # See all supported file types
88
+ codestamp list-types
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Commands
94
+
95
+ | Command | Description |
96
+ |---|---|
97
+ | `codestamp` | Process files added/modified in the last git commit (default) |
98
+ | `codestamp --bulk` | Process all source files in the project tree |
99
+ | `codestamp --staged` | Process only currently git-staged files |
100
+ | `codestamp --dry-run` | Preview all actions without writing any files |
101
+ | `codestamp update "<text>"` | Replace old copyright header with new text everywhere |
102
+ | `codestamp list-types` | Show all supported file extensions and comment styles |
103
+
104
+ ### Options
105
+
106
+ | Flag | Default | Description |
107
+ |---|---|---|
108
+ | `--license <path>` | `license.txt` | Custom path to your license/copyright file |
109
+ | `--exclude <path>` | _(none)_ | Path to an exclusion file (see below) |
110
+ | `--dry-run` | off | Preview mode — no files are written |
111
+
112
+ ---
113
+
114
+ ## Excluding files and directories
115
+
116
+ Pass `--exclude <file>` to point codestamp at a plain-text exclusion list. Each line can be a file path, a directory, a glob pattern, or a comment:
117
+
118
+ ```bash
119
+ # .csignore — paths codestamp should never touch
120
+
121
+ # Third-party files
122
+ vendor/google_utils.py
123
+ lib/react_helpers.js
124
+
125
+ # Entire directories
126
+ vendor/
127
+ third_party/
128
+ node_modules/
129
+
130
+ # Glob patterns
131
+ *.min.js
132
+ *.generated.py
133
+ ```
134
+
135
+ Then run:
136
+
137
+ ```bash
138
+ codestamp --bulk --exclude .csignore
139
+ ```
140
+
141
+ Rules supported per line:
142
+
143
+ | Pattern | Matches |
144
+ |---|---|
145
+ | `vendor/lib.py` | Exact file path |
146
+ | `vendor/` or `vendor` | Any file inside that directory |
147
+ | `*.min.js` | Glob matched against the full file path |
148
+ | `# comment` | Ignored |
149
+ | _(blank line)_ | Ignored |
150
+
151
+ ---
152
+
153
+ ## Output & status codes
154
+
155
+ Every file processed is reported with a status icon:
156
+
157
+ | Icon | Status | Meaning |
158
+ |---|---|---|
159
+ | ✅ | `ADDED` | Copyright header successfully stamped |
160
+ | 🔄 | `UPDATED` | Existing header replaced with new text |
161
+ | ⏭ | `SKIPPED` | File already has your copyright — left untouched |
162
+ | ➖ | `UNSUPPORTED` | File extension not recognised |
163
+ | ❌ | `ERROR` | Could not read or write the file |
164
+
165
+ ---
166
+
167
+ ## Supported file types
168
+
169
+ | Style | Extensions |
170
+ |---|---|
171
+ | `# comment` | `.py` `.sh` `.bash` `.zsh` `.rb` `.pl` `.r` `.yaml` `.yml` `.toml` `.conf` `.tf` `.tfvars` |
172
+ | `// comment` | `.js` `.ts` `.jsx` `.tsx` `.java` `.go` `.swift` `.kt` `.kts` `.rs` `.cs` `.cpp` `.cc` `.c` `.h` `.hpp` `.dart` `.scala` `.groovy` `.php` |
173
+ | `/* comment */` | `.css` `.scss` `.less` |
174
+ | `<!-- comment -->` | `.html` `.svelte` `.vue` `.xml` `.svg` |
175
+
176
+ Run `codestamp list-types` to see the full up-to-date list.
177
+
178
+ ---
179
+
180
+ ## How headers are inserted
181
+
182
+ - **Shebang lines** (`#!/usr/bin/env python3`) are always respected — the header is inserted on line 2, never before the shebang.
183
+ - Files that already contain your copyright text are silently **skipped**.
184
+ - The `update` command rewrites `license.txt` with the new header text after updating all files.
185
+
186
+ ---
187
+
188
+ ## Git integration
189
+
190
+ The default mode (no flags) processes files from the **last commit**, making it a perfect fit for CI pipelines:
191
+
192
+ ```yaml
193
+ # .github/workflows/copyright.yml
194
+ name: Copyright Headers
195
+
196
+ on: [push]
197
+
198
+ jobs:
199
+ stamp:
200
+ runs-on: ubuntu-latest
201
+ steps:
202
+ - uses: actions/checkout@v4
203
+ with:
204
+ fetch-depth: 2 # needed so HEAD~1 diff works
205
+
206
+ - name: Install codestamp
207
+ run: pip install codestamp
208
+
209
+ - name: Stamp new files
210
+ run: |
211
+ codestamp --exclude .csignore
212
+ git diff --quiet || (
213
+ git config user.email "bot@ci"
214
+ git config user.name "CI Bot"
215
+ git commit -am "chore: add copyright headers"
216
+ git push
217
+ )
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Contributing
223
+
224
+ ```bash
225
+ git clone https://github.com/yourusername/codestamp
226
+ cd codestamp
227
+ pip install -r requirements-dev.txt
228
+ pip install -e .
229
+ pytest
230
+ ```
231
+
232
+ Pull requests are welcome! Please open an issue first for larger changes.
233
+
234
+ ---
235
+
236
+ ## License
237
+
238
+ MIT © Shardul Kulkarni / Medhavee Inc
@@ -0,0 +1,210 @@
1
+ # codestamp
2
+
3
+ > Stamp and manage copyright headers across your source-code files.
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/codestamp)](https://pypi.org/project/codestamp/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/codestamp)](https://pypi.org/project/codestamp/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+
9
+ Built by **Medhavee Inc** · Author **Shardul Kulkarni**
10
+
11
+ ---
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install codestamp
17
+ ```
18
+
19
+ Or for local development:
20
+
21
+ ```bash
22
+ git clone https://github.com/yourusername/codestamp
23
+ cd codestamp
24
+ pip install -r requirements-dev.txt
25
+ pip install -e .
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Quick start
31
+
32
+ 1. Create a `license.txt` in your project root whose **first line** is your copyright statement:
33
+
34
+ ```
35
+ Copyright (c) 2026 Medhavee Inc. All rights reserved. Proprietary and confidential.
36
+ ```
37
+
38
+ 2. Run from the project root:
39
+
40
+ ```bash
41
+ # Add headers to files changed in the last git commit (CI-friendly default)
42
+ codestamp
43
+
44
+ # Add headers to ALL source files in the tree
45
+ codestamp --bulk
46
+
47
+ # Add headers to staged files only
48
+ codestamp --staged
49
+
50
+ # Skip specific files or directories using an exclusion file
51
+ codestamp --bulk --exclude .csignore
52
+
53
+ # Preview without writing anything
54
+ codestamp --bulk --dry-run
55
+
56
+ # Update every file's copyright to a new statement
57
+ codestamp update "Copyright (c) 2026 Medhavee Foundation. All rights reserved."
58
+
59
+ # See all supported file types
60
+ codestamp list-types
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Commands
66
+
67
+ | Command | Description |
68
+ |---|---|
69
+ | `codestamp` | Process files added/modified in the last git commit (default) |
70
+ | `codestamp --bulk` | Process all source files in the project tree |
71
+ | `codestamp --staged` | Process only currently git-staged files |
72
+ | `codestamp --dry-run` | Preview all actions without writing any files |
73
+ | `codestamp update "<text>"` | Replace old copyright header with new text everywhere |
74
+ | `codestamp list-types` | Show all supported file extensions and comment styles |
75
+
76
+ ### Options
77
+
78
+ | Flag | Default | Description |
79
+ |---|---|---|
80
+ | `--license <path>` | `license.txt` | Custom path to your license/copyright file |
81
+ | `--exclude <path>` | _(none)_ | Path to an exclusion file (see below) |
82
+ | `--dry-run` | off | Preview mode — no files are written |
83
+
84
+ ---
85
+
86
+ ## Excluding files and directories
87
+
88
+ Pass `--exclude <file>` to point codestamp at a plain-text exclusion list. Each line can be a file path, a directory, a glob pattern, or a comment:
89
+
90
+ ```bash
91
+ # .csignore — paths codestamp should never touch
92
+
93
+ # Third-party files
94
+ vendor/google_utils.py
95
+ lib/react_helpers.js
96
+
97
+ # Entire directories
98
+ vendor/
99
+ third_party/
100
+ node_modules/
101
+
102
+ # Glob patterns
103
+ *.min.js
104
+ *.generated.py
105
+ ```
106
+
107
+ Then run:
108
+
109
+ ```bash
110
+ codestamp --bulk --exclude .csignore
111
+ ```
112
+
113
+ Rules supported per line:
114
+
115
+ | Pattern | Matches |
116
+ |---|---|
117
+ | `vendor/lib.py` | Exact file path |
118
+ | `vendor/` or `vendor` | Any file inside that directory |
119
+ | `*.min.js` | Glob matched against the full file path |
120
+ | `# comment` | Ignored |
121
+ | _(blank line)_ | Ignored |
122
+
123
+ ---
124
+
125
+ ## Output & status codes
126
+
127
+ Every file processed is reported with a status icon:
128
+
129
+ | Icon | Status | Meaning |
130
+ |---|---|---|
131
+ | ✅ | `ADDED` | Copyright header successfully stamped |
132
+ | 🔄 | `UPDATED` | Existing header replaced with new text |
133
+ | ⏭ | `SKIPPED` | File already has your copyright — left untouched |
134
+ | ➖ | `UNSUPPORTED` | File extension not recognised |
135
+ | ❌ | `ERROR` | Could not read or write the file |
136
+
137
+ ---
138
+
139
+ ## Supported file types
140
+
141
+ | Style | Extensions |
142
+ |---|---|
143
+ | `# comment` | `.py` `.sh` `.bash` `.zsh` `.rb` `.pl` `.r` `.yaml` `.yml` `.toml` `.conf` `.tf` `.tfvars` |
144
+ | `// comment` | `.js` `.ts` `.jsx` `.tsx` `.java` `.go` `.swift` `.kt` `.kts` `.rs` `.cs` `.cpp` `.cc` `.c` `.h` `.hpp` `.dart` `.scala` `.groovy` `.php` |
145
+ | `/* comment */` | `.css` `.scss` `.less` |
146
+ | `<!-- comment -->` | `.html` `.svelte` `.vue` `.xml` `.svg` |
147
+
148
+ Run `codestamp list-types` to see the full up-to-date list.
149
+
150
+ ---
151
+
152
+ ## How headers are inserted
153
+
154
+ - **Shebang lines** (`#!/usr/bin/env python3`) are always respected — the header is inserted on line 2, never before the shebang.
155
+ - Files that already contain your copyright text are silently **skipped**.
156
+ - The `update` command rewrites `license.txt` with the new header text after updating all files.
157
+
158
+ ---
159
+
160
+ ## Git integration
161
+
162
+ The default mode (no flags) processes files from the **last commit**, making it a perfect fit for CI pipelines:
163
+
164
+ ```yaml
165
+ # .github/workflows/copyright.yml
166
+ name: Copyright Headers
167
+
168
+ on: [push]
169
+
170
+ jobs:
171
+ stamp:
172
+ runs-on: ubuntu-latest
173
+ steps:
174
+ - uses: actions/checkout@v4
175
+ with:
176
+ fetch-depth: 2 # needed so HEAD~1 diff works
177
+
178
+ - name: Install codestamp
179
+ run: pip install codestamp
180
+
181
+ - name: Stamp new files
182
+ run: |
183
+ codestamp --exclude .csignore
184
+ git diff --quiet || (
185
+ git config user.email "bot@ci"
186
+ git config user.name "CI Bot"
187
+ git commit -am "chore: add copyright headers"
188
+ git push
189
+ )
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Contributing
195
+
196
+ ```bash
197
+ git clone https://github.com/yourusername/codestamp
198
+ cd codestamp
199
+ pip install -r requirements-dev.txt
200
+ pip install -e .
201
+ pytest
202
+ ```
203
+
204
+ Pull requests are welcome! Please open an issue first for larger changes.
205
+
206
+ ---
207
+
208
+ ## License
209
+
210
+ MIT © Shardul Kulkarni / Medhavee Inc
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codestamp"
7
- version = "0.1.9"
7
+ version = "0.2.0"
8
8
  description = "CLI tool to manage copyright headers in source-code files"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -5,11 +5,12 @@ Manage copyright headers in source-code files.
5
5
 
6
6
  Usage examples
7
7
  --------------
8
- codestamp # git-diff mode (last commit)
9
- codestamp --bulk # all source files in tree
10
- codestamp --staged # only staged files
11
- codestamp --dry-run # preview, no writes
12
- codestamp update "Copyright 2026…" # update existing headers
8
+ codestamp # git-diff mode (last commit)
9
+ codestamp --bulk # all source files in tree
10
+ codestamp --staged # only staged files
11
+ codestamp --dry-run # preview, no writes
12
+ codestamp --exclude .csignore # skip paths listed in a file
13
+ codestamp update "Copyright 2026…" # update existing headers
13
14
  """
14
15
 
15
16
  from __future__ import annotations
@@ -41,14 +42,13 @@ _LOGO_LINES = [
41
42
  " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ",
42
43
  ]
43
44
 
44
- # Gradient: magenta → blue → cyan → green (top to bottom)
45
45
  _LOGO_COLORS = [
46
- ("magenta", True),
47
- ("bright_magenta",True),
48
- ("bright_blue", True),
49
- ("bright_cyan", True),
50
- ("cyan", True),
51
- ("bright_green", True),
46
+ ("magenta", True),
47
+ ("bright_magenta", True),
48
+ ("bright_blue", True),
49
+ ("bright_cyan", True),
50
+ ("cyan", True),
51
+ ("bright_green", True),
52
52
  ]
53
53
 
54
54
  _DIVIDER = " " + "─" * 76
@@ -58,23 +58,73 @@ def _print_banner() -> None:
58
58
  click.echo()
59
59
  for line, (color, bold) in zip(_LOGO_LINES, _LOGO_COLORS):
60
60
  click.echo(click.style(line, fg=color, bold=bold))
61
-
62
61
  click.echo()
63
-
64
- # Tagline — each segment in its own accent color
65
62
  by_label = click.style(" ✦ ", fg="bright_yellow", bold=True)
66
63
  org = click.style("Medhavee Inc", fg="bright_yellow", bold=True)
67
64
  pipe1 = click.style(" │ ", fg="bright_black")
68
65
  auth_label = click.style("author ", fg="bright_black")
69
66
  author = click.style("Shardul Kulkarni", fg="bright_white", bold=True)
70
67
  pipe2 = click.style(" │ ", fg="bright_black")
71
- version = click.style("v0.1.0", fg="bright_green")
72
-
68
+ version = click.style("v0.2.0", fg="bright_green")
73
69
  click.echo(by_label + org + pipe1 + auth_label + author + pipe2 + version)
74
70
  click.echo(click.style(_DIVIDER, fg="bright_black"))
75
71
  click.echo()
76
72
 
77
73
 
74
+ # ---------------------------------------------------------------------------
75
+ # Exclusion list loader
76
+ # ---------------------------------------------------------------------------
77
+
78
+ def _load_exclusions(exclude_file: str | None) -> set[str]:
79
+ """
80
+ Parse an exclusion file and return a set of normalised path strings.
81
+
82
+ Each line can be:
83
+ - a file path e.g. vendor/lib.py
84
+ - a directory e.g. third_party/
85
+ - a glob-style e.g. *.min.js (matched against the full path)
86
+ - a comment e.g. # this is ignored
87
+ - blank line (ignored)
88
+ """
89
+ if not exclude_file:
90
+ return set()
91
+
92
+ p = Path(exclude_file)
93
+ if not p.exists():
94
+ click.echo(click.style(f" ✖ Exclusion file '{exclude_file}' not found.", fg="bright_red"), err=True)
95
+ sys.exit(1)
96
+
97
+ exclusions: set[str] = set()
98
+ for raw in p.read_text(encoding="utf-8").splitlines():
99
+ line = raw.strip()
100
+ if not line or line.startswith("#"):
101
+ continue
102
+ exclusions.add(line.rstrip("/")) # normalise trailing slash on dirs
103
+ return exclusions
104
+
105
+
106
+ def _is_excluded(filepath: str, exclusions: set[str]) -> bool:
107
+ """Return True if *filepath* matches any entry in *exclusions*."""
108
+ if not exclusions:
109
+ return False
110
+ p = Path(filepath)
111
+ for entry in exclusions:
112
+ entry_path = Path(entry)
113
+ # Exact file match
114
+ if p == entry_path:
115
+ return True
116
+ # Directory prefix match (e.g. "vendor" excludes "vendor/foo/bar.py")
117
+ try:
118
+ p.relative_to(entry_path)
119
+ return True
120
+ except ValueError:
121
+ pass
122
+ # Glob match against full path string
123
+ if p.match(entry):
124
+ return True
125
+ return False
126
+
127
+
78
128
  # ---------------------------------------------------------------------------
79
129
  # Helpers
80
130
  # ---------------------------------------------------------------------------
@@ -101,12 +151,12 @@ def _collect_all_files(root: str = ".") -> list[str]:
101
151
  def _read_license(license_path: str) -> str:
102
152
  p = Path(license_path)
103
153
  if not p.exists():
104
- click.echo(click.style(f" ✖ '{license_path}' not found.", fg="red", bold=True), err=True)
154
+ click.echo(click.style(f" ✖ '{license_path}' not found.", fg="bright_red", bold=True), err=True)
105
155
  click.echo(click.style(" Tip: run from the project root, or pass --license <path>", fg="bright_black"), err=True)
106
156
  sys.exit(1)
107
157
  first = p.read_text(encoding="utf-8").splitlines()[0].strip()
108
158
  if not first:
109
- click.echo(click.style(f" ✖ '{license_path}' is empty.", fg="red", bold=True), err=True)
159
+ click.echo(click.style(f" ✖ '{license_path}' is empty.", fg="bright_red", bold=True), err=True)
110
160
  sys.exit(1)
111
161
  return first
112
162
 
@@ -136,13 +186,13 @@ def _print_results(results: list[Result]) -> None:
136
186
  errors = sum(1 for r in results if r.status == Status.ERROR)
137
187
 
138
188
  click.echo()
139
- click.echo(click.style(" " + "─" * 50, fg="bright_black"))
189
+ click.echo(click.style(" " + "─" * 55, fg="bright_black"))
140
190
  click.echo(
141
191
  " "
142
192
  + click.style(f"✅ {added} added ", fg="bright_green", bold=True)
143
193
  + click.style(f"🔄 {updated} updated ", fg="bright_cyan", bold=True)
144
194
  + click.style(f"⏭ {skipped} skipped ", fg="yellow", bold=True)
145
- + click.style(f"❌ {errors} errors", fg="bright_red" if errors else "bright_black", bold=bool(errors))
195
+ + click.style(f"❌ {errors} errors", fg="bright_red" if errors else "bright_black", bold=bool(errors))
146
196
  )
147
197
  click.echo()
148
198
 
@@ -153,12 +203,15 @@ def _print_results(results: list[Result]) -> None:
153
203
 
154
204
  @click.group(invoke_without_command=True, context_settings={"help_option_names": ["-h", "--help"]})
155
205
  @click.pass_context
156
- @click.option("--license", "license_path", default="license.txt", show_default=True,
206
+ @click.option("--license", "license_path", default="license.txt", show_default=True,
157
207
  help="Path to license.txt (first line = copyright text).")
208
+ @click.option("--exclude", "exclude_file", default=None,
209
+ help="Path to a file listing paths/dirs to exclude (one per line).")
158
210
  @click.option("--bulk", is_flag=True, help="Process ALL source files in the current tree.")
159
211
  @click.option("--staged", is_flag=True, help="Process only currently staged git files.")
160
212
  @click.option("--dry-run", is_flag=True, help="Preview actions without writing any files.")
161
- def cli(ctx: click.Context, license_path: str, bulk: bool, staged: bool, dry_run: bool) -> None:
213
+ def cli(ctx: click.Context, license_path: str, exclude_file: str | None,
214
+ bulk: bool, staged: bool, dry_run: bool) -> None:
162
215
  """Stamp copyright headers onto your source-code files.\n
163
216
  Default mode: processes files added/modified in the last git commit.
164
217
  """
@@ -168,7 +221,11 @@ def cli(ctx: click.Context, license_path: str, bulk: bool, staged: bool, dry_run
168
221
  return
169
222
 
170
223
  copyright_text = _read_license(license_path)
224
+ exclusions = _load_exclusions(exclude_file)
225
+
171
226
  _kv("Copyright :", copyright_text, "bright_white")
227
+ if exclusions:
228
+ _kv("Excluding :", f"{len(exclusions)} rule(s) from {exclude_file}", "yellow")
172
229
 
173
230
  if dry_run:
174
231
  click.echo(click.style("\n ⚠ Dry-run mode — no files will be changed.", fg="bright_yellow"))
@@ -191,6 +248,10 @@ def cli(ctx: click.Context, license_path: str, bulk: bool, staged: bool, dry_run
191
248
  mode = "GIT DIFF — files in last commit"
192
249
  files = get_last_commit_files()
193
250
 
251
+ # Apply exclusions
252
+ if exclusions:
253
+ files = [f for f in files if not _is_excluded(f, exclusions)]
254
+
194
255
  _kv("Mode :", mode, "bright_cyan")
195
256
  _kv("Files :", str(len(files)), "bright_white")
196
257
 
@@ -210,26 +271,34 @@ def cli(ctx: click.Context, license_path: str, bulk: bool, staged: bool, dry_run
210
271
  @click.argument("new_header")
211
272
  @click.option("--license", "license_path", default="license.txt", show_default=True,
212
273
  help="Path to license.txt (first line is the OLD copyright text).")
274
+ @click.option("--exclude", "exclude_file", default=None,
275
+ help="Path to a file listing paths/dirs to exclude (one per line).")
213
276
  @click.option("--dry-run", is_flag=True, help="Preview without writing.")
214
- def update(new_header: str, license_path: str, dry_run: bool) -> None:
277
+ def update(new_header: str, license_path: str, exclude_file: str | None, dry_run: bool) -> None:
215
278
  """Replace existing copyright headers with NEW_HEADER across all source files.
216
279
 
217
280
  Also rewrites license.txt with the new header (unless --dry-run).
218
281
 
219
282
  \b
220
283
  Example:
221
- codestamp update "Copyright (c) 2026 Acme Inc. All rights reserved."
284
+ codestamp update "Copyright (c) 2026 Medhavee Foundation. All rights reserved."
222
285
  """
223
286
  old_copyright = _read_license(license_path)
287
+ exclusions = _load_exclusions(exclude_file)
224
288
 
225
289
  _kv("Old :", old_copyright, "bright_black")
226
290
  _kv("New :", new_header, "bright_white")
291
+ if exclusions:
292
+ _kv("Excluding :", f"{len(exclusions)} rule(s) from {exclude_file}", "yellow")
227
293
 
228
294
  if dry_run:
229
295
  click.echo(click.style("\n ⚠ Dry-run mode — no files will be changed.", fg="bright_yellow"))
230
296
  click.echo()
231
297
 
232
298
  files = _collect_all_files()
299
+ if exclusions:
300
+ files = [f for f in files if not _is_excluded(f, exclusions)]
301
+
233
302
  if not files:
234
303
  click.echo(click.style(" No files found.", fg="yellow"))
235
304
  return