combicode 2.0.0__tar.gz → 3.0.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.
- combicode-3.0.0/PKG-INFO +166 -0
- combicode-3.0.0/README.md +144 -0
- combicode-3.0.0/combicode/__init__.py +1 -0
- combicode-3.0.0/combicode/archive.py +128 -0
- combicode-3.0.0/combicode/ignore.json +53 -0
- combicode-3.0.0/combicode/main.py +458 -0
- combicode-3.0.0/combicode/parsers.py +966 -0
- combicode-3.0.0/combicode.egg-info/PKG-INFO +166 -0
- {combicode-2.0.0 → combicode-3.0.0}/combicode.egg-info/SOURCES.txt +7 -1
- combicode-3.0.0/combicode.egg-info/requires.txt +6 -0
- {combicode-2.0.0 → combicode-3.0.0}/pyproject.toml +19 -6
- combicode-3.0.0/tests/test_archive.py +149 -0
- combicode-3.0.0/tests/test_main.py +489 -0
- combicode-3.0.0/tests/test_regressions.py +119 -0
- combicode-3.0.0/tests/test_release.py +40 -0
- combicode-2.0.0/PKG-INFO +0 -488
- combicode-2.0.0/README.md +0 -445
- combicode-2.0.0/combicode/__init__.py +0 -1
- combicode-2.0.0/combicode/main.py +0 -1299
- combicode-2.0.0/combicode.egg-info/PKG-INFO +0 -488
- combicode-2.0.0/combicode.egg-info/requires.txt +0 -5
- combicode-2.0.0/tests/test_main.py +0 -492
- {combicode-2.0.0 → combicode-3.0.0}/LICENSE +0 -0
- {combicode-2.0.0 → combicode-3.0.0}/combicode.egg-info/dependency_links.txt +0 -0
- {combicode-2.0.0 → combicode-3.0.0}/combicode.egg-info/entry_points.txt +0 -0
- {combicode-2.0.0 → combicode-3.0.0}/combicode.egg-info/top_level.txt +0 -0
- {combicode-2.0.0 → combicode-3.0.0}/setup.cfg +0 -0
combicode-3.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: combicode
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: A CLI tool to combine a project's codebase into a single file for LLM context with expanded code map.
|
|
5
|
+
Author: A. Aurelions
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aaurelions/combicode
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/aaurelions/combicode/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Utilities
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: click>=8.5.0
|
|
17
|
+
Requires-Dist: pathspec>=1.1.1
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest>=9.1.1; extra == "test"
|
|
20
|
+
Requires-Dist: ruff>=0.16.7; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Combicode
|
|
24
|
+
|
|
25
|
+
[](https://www.npmjs.com/package/combicode)
|
|
26
|
+
[](https://pypi.org/project/combicode/)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
|
|
29
|
+
Combine a project's UTF-8 source files into one readable context file, with a directory tree and code outline. Restore those files with either the JavaScript or Python CLI.
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
Requires **Node.js 22.12+** or **Python 3.11+**.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npx combicode
|
|
37
|
+
# or
|
|
38
|
+
pipx run combicode
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Run from the project root. The default output is `combicode.txt`.
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
# Preview without writing
|
|
45
|
+
combicode --dry-run
|
|
46
|
+
|
|
47
|
+
# Include selected file types (case-insensitive)
|
|
48
|
+
combicode -i '.ts, .tsx, .css' -o output/context.txt
|
|
49
|
+
|
|
50
|
+
# Add exclusions
|
|
51
|
+
combicode -e '**/*.generated.*,fixtures/'
|
|
52
|
+
|
|
53
|
+
# Keep files in the tree but omit their contents
|
|
54
|
+
combicode --skip-content '**/*.test.ts,**/*.spec.ts'
|
|
55
|
+
|
|
56
|
+
# Disable code outline parsing
|
|
57
|
+
combicode --no-parse
|
|
58
|
+
|
|
59
|
+
# Use a documentation-oriented prompt
|
|
60
|
+
combicode --llms-txt -i .md -o documentation.txt
|
|
61
|
+
|
|
62
|
+
# Restore an archive into a directory
|
|
63
|
+
combicode --recreate --input documentation.txt -o restored
|
|
64
|
+
combicode --recreate --input documentation.txt -o restored --dry-run
|
|
65
|
+
combicode --recreate --input documentation.txt -o restored --overwrite
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`-i` always means `--include-ext`; use `--input` for restoration. `--llms-txt` changes the introductory prompt only: it does not read links or fetch documentation.
|
|
69
|
+
|
|
70
|
+
## Options
|
|
71
|
+
|
|
72
|
+
| Option | Alias | Purpose | Default |
|
|
73
|
+
| --- | --- | --- | --- |
|
|
74
|
+
| `--output` | `-o` | Output file, or restore directory | `combicode.txt`; current directory in restore mode |
|
|
75
|
+
| `--dry-run` | `-d` | Preview without writing | Off |
|
|
76
|
+
| `--include-ext` | `-i` | Comma-separated extensions | All |
|
|
77
|
+
| `--exclude` | `-e` | Additional comma-separated gitignore patterns | None |
|
|
78
|
+
| `--skip-content` | | Show matching files in the tree but omit content | None |
|
|
79
|
+
| `--no-parse` | | Disable code outline parsing | Parsing enabled |
|
|
80
|
+
| `--no-gitignore` | | Ignore project `.gitignore` files | Respect `.gitignore` |
|
|
81
|
+
| `--no-header` | | Omit the prompt and code index | Header enabled |
|
|
82
|
+
| `--llms-txt` | `-l` | Use a documentation prompt | Off |
|
|
83
|
+
| `--recreate` | `-r` | Restore files from an archive | Off |
|
|
84
|
+
| `--input` | | Archive to restore | `combicode.txt` |
|
|
85
|
+
| `--overwrite` | | Replace existing files during restoration | Off |
|
|
86
|
+
| `--version` | `-v` | Print version | |
|
|
87
|
+
| `--help` | `-h` | Print help | |
|
|
88
|
+
|
|
89
|
+
Output parent directories are created automatically. Unknown options fail with a nonzero exit status.
|
|
90
|
+
|
|
91
|
+
## File selection
|
|
92
|
+
|
|
93
|
+
Both packages use the shared rules in [configs/ignore.json](configs/ignore.json). These exclude dependency folders, build outputs, common binary formats, `.git`, `.env`, and other generated files. Defaults and explicit `--exclude` patterns take precedence over project `.gitignore` files; `--no-gitignore` does not disable these defaults.
|
|
94
|
+
|
|
95
|
+
Nested `.gitignore` files can override patterns from parent files. An ignored directory is not traversed, so a child rule cannot re-include a file inside it. Submodule paths from `.gitmodules` are excluded. Symlinks and non-regular files are not collected. The output file excludes itself.
|
|
96
|
+
|
|
97
|
+
Files with NUL bytes in the initial sample are treated as binary. Other selected files must decode as UTF-8; read or decoding failures stop generation without replacing an existing output. Ignore rules are file-selection defaults, not secret detection; inspect the preview before sharing project content.
|
|
98
|
+
|
|
99
|
+
## Code outline and line references
|
|
100
|
+
|
|
101
|
+
The output consists of an optional prompt and `<code_index>`, followed by `<merged_code>` records. Entries show:
|
|
102
|
+
|
|
103
|
+
- **OL**: original source line numbers.
|
|
104
|
+
- **ML**: line numbers within the combined output.
|
|
105
|
+
- **Size**: UTF-8 byte size.
|
|
106
|
+
|
|
107
|
+
Empty files and omitted content use `OL: 0-0`; their ML range points to the blank or placeholder line in the output.
|
|
108
|
+
|
|
109
|
+
For example, `ML: 53-57` refers to lines 53 through 57 in the output:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
sed -n '53,57p' combicode.txt
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The Python package uses Python's built-in AST for valid Python sources. Other outlines, including Python outlines produced by the JavaScript package, use heuristics. They cover these language families:
|
|
116
|
+
|
|
117
|
+
| Languages | Typical outline elements |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| Python | Classes, functions, async functions, loops |
|
|
120
|
+
| JavaScript, TypeScript | Classes, functions, methods, interfaces, test suites |
|
|
121
|
+
| Go, Rust | Types, functions, implementation blocks |
|
|
122
|
+
| Java, C/C++, C# | Types and methods |
|
|
123
|
+
| PHP, Ruby | Classes, modules, functions |
|
|
124
|
+
| Swift, Kotlin, Scala | Types and functions |
|
|
125
|
+
| Lua, Perl, shell scripts | Functions and some control-flow blocks |
|
|
126
|
+
|
|
127
|
+
Configuration, markup, and other text files are included without a code outline. Heuristic outlines are approximate: unusual syntax, multiline declarations, regular-expression literals, and language-specific constructs can be missed or assigned imperfect ranges. File content remains intact. Use `--no-parse` when only the file tree is needed.
|
|
128
|
+
|
|
129
|
+
## Archive format and restoration
|
|
130
|
+
|
|
131
|
+
Each record includes a percent-encoded relative path, OL/ML information, exact UTF-8 byte length, and an explicit omission flag. Four-backtick fences keep the output readable; the byte length, rather than the fences, delimits the original content.
|
|
132
|
+
|
|
133
|
+
This preserves empty files, final newlines, CRLF, Unicode, and source text containing archive-like headers or Markdown fences. JavaScript and Python archives are interchangeable. Omitted records are never restored, and existing files are skipped unless `--overwrite` is supplied. Summaries count files actually written or eligible for writing in a dry run.
|
|
134
|
+
|
|
135
|
+
Restoration validates records and paths before writing. Absolute paths, traversal components, duplicate names, control characters, backslashes, colon-containing paths, trailing dots/spaces, and Windows device names are rejected. Symlinks at or below the destination are rejected. Each file is staged and atomically installed; overwriting a hardlink does not change its other links. This is not a whole-project transaction: an I/O failure can leave earlier files restored. Use a destination that is not being modified concurrently by another process.
|
|
136
|
+
|
|
137
|
+
**Old archive formats are unsupported. Regenerate existing context files before restoring them.** Archives preserve file content, not permissions, timestamps, symlinks, or binary files. Editing a record's content requires updating its byte length.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
bash scripts/sync-assets.sh
|
|
143
|
+
npm ci --prefix combicode-js
|
|
144
|
+
npm test --prefix combicode-js
|
|
145
|
+
python3 -m venv .venv
|
|
146
|
+
.venv/bin/python -m pip install -e './combicode-py[test]' build
|
|
147
|
+
.venv/bin/python -m pytest -q combicode-py
|
|
148
|
+
.venv/bin/python -m ruff check --config combicode-py/pyproject.toml combicode-py scripts
|
|
149
|
+
.venv/bin/python -m build combicode-py
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
On Windows, use the corresponding executables under `.venv/Scripts`. The Python cross-runtime tests also require Node.js and installed JavaScript dependencies; they skip when Node.js is unavailable. CI installs both runtimes and tests minimum supported versions plus current runtimes on Linux, macOS, and Windows.
|
|
153
|
+
|
|
154
|
+
Structure:
|
|
155
|
+
|
|
156
|
+
- `combicode-js/index.js` and `combicode-py/combicode/main.py`: CLI, file selection, and index generation.
|
|
157
|
+
- `lib/parsers.js` and `combicode/parsers.py`: language outlines.
|
|
158
|
+
- `lib/archive.js` and `combicode/archive.py`: framing, validation, and restoration.
|
|
159
|
+
- `tests/fixtures`: shared round-trip cases.
|
|
160
|
+
- `configs/ignore.json`: canonical defaults, synchronized into both packages.
|
|
161
|
+
|
|
162
|
+
Run `python3 scripts/prepare-release.py MAJOR.MINOR.PATCH` to update both package versions and the npm lockfile. Update changelogs and review changes before tagging and publishing a release. Release workflows synchronize shared assets, run package tests, build distributions, and publish.
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Combicode
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/combicode)
|
|
4
|
+
[](https://pypi.org/project/combicode/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Combine a project's UTF-8 source files into one readable context file, with a directory tree and code outline. Restore those files with either the JavaScript or Python CLI.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
Requires **Node.js 22.12+** or **Python 3.11+**.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx combicode
|
|
15
|
+
# or
|
|
16
|
+
pipx run combicode
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Run from the project root. The default output is `combicode.txt`.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
# Preview without writing
|
|
23
|
+
combicode --dry-run
|
|
24
|
+
|
|
25
|
+
# Include selected file types (case-insensitive)
|
|
26
|
+
combicode -i '.ts, .tsx, .css' -o output/context.txt
|
|
27
|
+
|
|
28
|
+
# Add exclusions
|
|
29
|
+
combicode -e '**/*.generated.*,fixtures/'
|
|
30
|
+
|
|
31
|
+
# Keep files in the tree but omit their contents
|
|
32
|
+
combicode --skip-content '**/*.test.ts,**/*.spec.ts'
|
|
33
|
+
|
|
34
|
+
# Disable code outline parsing
|
|
35
|
+
combicode --no-parse
|
|
36
|
+
|
|
37
|
+
# Use a documentation-oriented prompt
|
|
38
|
+
combicode --llms-txt -i .md -o documentation.txt
|
|
39
|
+
|
|
40
|
+
# Restore an archive into a directory
|
|
41
|
+
combicode --recreate --input documentation.txt -o restored
|
|
42
|
+
combicode --recreate --input documentation.txt -o restored --dry-run
|
|
43
|
+
combicode --recreate --input documentation.txt -o restored --overwrite
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`-i` always means `--include-ext`; use `--input` for restoration. `--llms-txt` changes the introductory prompt only: it does not read links or fetch documentation.
|
|
47
|
+
|
|
48
|
+
## Options
|
|
49
|
+
|
|
50
|
+
| Option | Alias | Purpose | Default |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| `--output` | `-o` | Output file, or restore directory | `combicode.txt`; current directory in restore mode |
|
|
53
|
+
| `--dry-run` | `-d` | Preview without writing | Off |
|
|
54
|
+
| `--include-ext` | `-i` | Comma-separated extensions | All |
|
|
55
|
+
| `--exclude` | `-e` | Additional comma-separated gitignore patterns | None |
|
|
56
|
+
| `--skip-content` | | Show matching files in the tree but omit content | None |
|
|
57
|
+
| `--no-parse` | | Disable code outline parsing | Parsing enabled |
|
|
58
|
+
| `--no-gitignore` | | Ignore project `.gitignore` files | Respect `.gitignore` |
|
|
59
|
+
| `--no-header` | | Omit the prompt and code index | Header enabled |
|
|
60
|
+
| `--llms-txt` | `-l` | Use a documentation prompt | Off |
|
|
61
|
+
| `--recreate` | `-r` | Restore files from an archive | Off |
|
|
62
|
+
| `--input` | | Archive to restore | `combicode.txt` |
|
|
63
|
+
| `--overwrite` | | Replace existing files during restoration | Off |
|
|
64
|
+
| `--version` | `-v` | Print version | |
|
|
65
|
+
| `--help` | `-h` | Print help | |
|
|
66
|
+
|
|
67
|
+
Output parent directories are created automatically. Unknown options fail with a nonzero exit status.
|
|
68
|
+
|
|
69
|
+
## File selection
|
|
70
|
+
|
|
71
|
+
Both packages use the shared rules in [configs/ignore.json](configs/ignore.json). These exclude dependency folders, build outputs, common binary formats, `.git`, `.env`, and other generated files. Defaults and explicit `--exclude` patterns take precedence over project `.gitignore` files; `--no-gitignore` does not disable these defaults.
|
|
72
|
+
|
|
73
|
+
Nested `.gitignore` files can override patterns from parent files. An ignored directory is not traversed, so a child rule cannot re-include a file inside it. Submodule paths from `.gitmodules` are excluded. Symlinks and non-regular files are not collected. The output file excludes itself.
|
|
74
|
+
|
|
75
|
+
Files with NUL bytes in the initial sample are treated as binary. Other selected files must decode as UTF-8; read or decoding failures stop generation without replacing an existing output. Ignore rules are file-selection defaults, not secret detection; inspect the preview before sharing project content.
|
|
76
|
+
|
|
77
|
+
## Code outline and line references
|
|
78
|
+
|
|
79
|
+
The output consists of an optional prompt and `<code_index>`, followed by `<merged_code>` records. Entries show:
|
|
80
|
+
|
|
81
|
+
- **OL**: original source line numbers.
|
|
82
|
+
- **ML**: line numbers within the combined output.
|
|
83
|
+
- **Size**: UTF-8 byte size.
|
|
84
|
+
|
|
85
|
+
Empty files and omitted content use `OL: 0-0`; their ML range points to the blank or placeholder line in the output.
|
|
86
|
+
|
|
87
|
+
For example, `ML: 53-57` refers to lines 53 through 57 in the output:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
sed -n '53,57p' combicode.txt
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The Python package uses Python's built-in AST for valid Python sources. Other outlines, including Python outlines produced by the JavaScript package, use heuristics. They cover these language families:
|
|
94
|
+
|
|
95
|
+
| Languages | Typical outline elements |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| Python | Classes, functions, async functions, loops |
|
|
98
|
+
| JavaScript, TypeScript | Classes, functions, methods, interfaces, test suites |
|
|
99
|
+
| Go, Rust | Types, functions, implementation blocks |
|
|
100
|
+
| Java, C/C++, C# | Types and methods |
|
|
101
|
+
| PHP, Ruby | Classes, modules, functions |
|
|
102
|
+
| Swift, Kotlin, Scala | Types and functions |
|
|
103
|
+
| Lua, Perl, shell scripts | Functions and some control-flow blocks |
|
|
104
|
+
|
|
105
|
+
Configuration, markup, and other text files are included without a code outline. Heuristic outlines are approximate: unusual syntax, multiline declarations, regular-expression literals, and language-specific constructs can be missed or assigned imperfect ranges. File content remains intact. Use `--no-parse` when only the file tree is needed.
|
|
106
|
+
|
|
107
|
+
## Archive format and restoration
|
|
108
|
+
|
|
109
|
+
Each record includes a percent-encoded relative path, OL/ML information, exact UTF-8 byte length, and an explicit omission flag. Four-backtick fences keep the output readable; the byte length, rather than the fences, delimits the original content.
|
|
110
|
+
|
|
111
|
+
This preserves empty files, final newlines, CRLF, Unicode, and source text containing archive-like headers or Markdown fences. JavaScript and Python archives are interchangeable. Omitted records are never restored, and existing files are skipped unless `--overwrite` is supplied. Summaries count files actually written or eligible for writing in a dry run.
|
|
112
|
+
|
|
113
|
+
Restoration validates records and paths before writing. Absolute paths, traversal components, duplicate names, control characters, backslashes, colon-containing paths, trailing dots/spaces, and Windows device names are rejected. Symlinks at or below the destination are rejected. Each file is staged and atomically installed; overwriting a hardlink does not change its other links. This is not a whole-project transaction: an I/O failure can leave earlier files restored. Use a destination that is not being modified concurrently by another process.
|
|
114
|
+
|
|
115
|
+
**Old archive formats are unsupported. Regenerate existing context files before restoring them.** Archives preserve file content, not permissions, timestamps, symlinks, or binary files. Editing a record's content requires updating its byte length.
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
bash scripts/sync-assets.sh
|
|
121
|
+
npm ci --prefix combicode-js
|
|
122
|
+
npm test --prefix combicode-js
|
|
123
|
+
python3 -m venv .venv
|
|
124
|
+
.venv/bin/python -m pip install -e './combicode-py[test]' build
|
|
125
|
+
.venv/bin/python -m pytest -q combicode-py
|
|
126
|
+
.venv/bin/python -m ruff check --config combicode-py/pyproject.toml combicode-py scripts
|
|
127
|
+
.venv/bin/python -m build combicode-py
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
On Windows, use the corresponding executables under `.venv/Scripts`. The Python cross-runtime tests also require Node.js and installed JavaScript dependencies; they skip when Node.js is unavailable. CI installs both runtimes and tests minimum supported versions plus current runtimes on Linux, macOS, and Windows.
|
|
131
|
+
|
|
132
|
+
Structure:
|
|
133
|
+
|
|
134
|
+
- `combicode-js/index.js` and `combicode-py/combicode/main.py`: CLI, file selection, and index generation.
|
|
135
|
+
- `lib/parsers.js` and `combicode/parsers.py`: language outlines.
|
|
136
|
+
- `lib/archive.js` and `combicode/archive.py`: framing, validation, and restoration.
|
|
137
|
+
- `tests/fixtures`: shared round-trip cases.
|
|
138
|
+
- `configs/ignore.json`: canonical defaults, synchronized into both packages.
|
|
139
|
+
|
|
140
|
+
Run `python3 scripts/prepare-release.py MAJOR.MINOR.PATCH` to update both package versions and the npm lockfile. Update changelogs and review changes before tagging and publishing a release. Release workflows synchronize shared assets, run package tests, build distributions, and publish.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.0.0"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Length-framed UTF-8 archives. File bodies are opaque bytes during extraction."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import quote, unquote
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def encode_path(name):
|
|
11
|
+
return quote(name, safe="/!~*'()")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def record(name, metadata, content):
|
|
15
|
+
validate_path(name)
|
|
16
|
+
omitted = content is None
|
|
17
|
+
body = "(Content omitted)" if omitted else content
|
|
18
|
+
size = len(body.encode("utf-8"))
|
|
19
|
+
separator = "" if body.endswith("\n") else "\n"
|
|
20
|
+
return (
|
|
21
|
+
f"# FILE: {encode_path(name)} [{metadata}] [BYTES: {size} | OMITTED: {int(omitted)}]\n"
|
|
22
|
+
f"````\n{body}{separator}````\n\n"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_path(name):
|
|
27
|
+
if (
|
|
28
|
+
not name
|
|
29
|
+
or re.search(r"[\\:\x00-\x1f\x7f]", name)
|
|
30
|
+
or any(
|
|
31
|
+
part in ("", ".", "..")
|
|
32
|
+
or part.endswith((".", " "))
|
|
33
|
+
or re.match(r"(?i)^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)", part)
|
|
34
|
+
for part in name.split("/")
|
|
35
|
+
)
|
|
36
|
+
):
|
|
37
|
+
raise ValueError(f"Unsafe archive path: {name!r}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_archive(data):
|
|
41
|
+
opening = b"<merged_code>\n"
|
|
42
|
+
offset = data.find(opening)
|
|
43
|
+
if offset < 0 or (offset > 0 and data[offset - 1] != 10):
|
|
44
|
+
raise ValueError("Missing merged_code section")
|
|
45
|
+
offset += len(opening)
|
|
46
|
+
files, seen = [], set()
|
|
47
|
+
closing = b"</merged_code>\n"
|
|
48
|
+
while not data.startswith(closing, offset):
|
|
49
|
+
end = data.find(b"\n", offset)
|
|
50
|
+
if end < 0:
|
|
51
|
+
raise ValueError("Incomplete archive")
|
|
52
|
+
match = re.fullmatch(
|
|
53
|
+
rb"# FILE: (\S+) \[OL: \d+-\d+ \| ML: \d+-\d+ \| [^\]]+\] "
|
|
54
|
+
rb"\[BYTES: (\d+) \| OMITTED: ([01])\]",
|
|
55
|
+
data[offset:end],
|
|
56
|
+
)
|
|
57
|
+
if not match:
|
|
58
|
+
raise ValueError("Invalid archive record (old formats are unsupported)")
|
|
59
|
+
if re.search(rb"%(?![0-9a-fA-F]{2})", match[1]):
|
|
60
|
+
raise ValueError("Invalid percent-encoded path")
|
|
61
|
+
name = unquote(match[1].decode("ascii"), errors="strict")
|
|
62
|
+
validate_path(name)
|
|
63
|
+
if name in seen:
|
|
64
|
+
raise ValueError(f"Duplicate archive path: {name}")
|
|
65
|
+
seen.add(name)
|
|
66
|
+
length = int(match[2])
|
|
67
|
+
offset = end + 1
|
|
68
|
+
if data[offset : offset + 5] != b"````\n":
|
|
69
|
+
raise ValueError("Missing opening fence")
|
|
70
|
+
offset += 5
|
|
71
|
+
content = data[offset : offset + length]
|
|
72
|
+
if len(content) != length:
|
|
73
|
+
raise ValueError("Truncated archive record")
|
|
74
|
+
offset += length
|
|
75
|
+
footer = b"````\n\n" if content.endswith(b"\n") else b"\n````\n\n"
|
|
76
|
+
if data[offset : offset + len(footer)] != footer:
|
|
77
|
+
raise ValueError("Invalid record ending")
|
|
78
|
+
offset += len(footer)
|
|
79
|
+
if match[3] == b"0":
|
|
80
|
+
files.append((name, content))
|
|
81
|
+
if offset + len(closing) != len(data):
|
|
82
|
+
raise ValueError("Unexpected data after merged_code section")
|
|
83
|
+
for name in seen:
|
|
84
|
+
parts = name.split("/")
|
|
85
|
+
if any("/".join(parts[:i]) in seen for i in range(1, len(parts))):
|
|
86
|
+
raise ValueError(f"Conflicting archive paths: {name}")
|
|
87
|
+
if not seen:
|
|
88
|
+
raise ValueError("No files found in the input file")
|
|
89
|
+
return files
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def safe_target(root, name):
|
|
93
|
+
validate_path(name)
|
|
94
|
+
target = root / name
|
|
95
|
+
components = [root]
|
|
96
|
+
for part in name.split("/"):
|
|
97
|
+
components.append(components[-1] / part)
|
|
98
|
+
for component in components:
|
|
99
|
+
if component.is_symlink():
|
|
100
|
+
raise ValueError(f"Symlink destination: {component}")
|
|
101
|
+
return target
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def restore(files, output_dir, dry_run, overwrite):
|
|
105
|
+
root = Path(os.path.abspath(output_dir))
|
|
106
|
+
targets = [(safe_target(root, name), name, content) for name, content in files]
|
|
107
|
+
count = size = 0
|
|
108
|
+
for target, name, content in targets:
|
|
109
|
+
if target.exists() and not overwrite:
|
|
110
|
+
continue
|
|
111
|
+
if not dry_run:
|
|
112
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
safe_target(root, name)
|
|
114
|
+
atomic_write(target, content, overwrite)
|
|
115
|
+
|
|
116
|
+
count += 1
|
|
117
|
+
size += len(content)
|
|
118
|
+
return count, size
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def atomic_write(target, content, overwrite=True):
|
|
122
|
+
with tempfile.TemporaryDirectory(prefix=".combicode-", dir=target.parent) as directory:
|
|
123
|
+
staged = Path(directory) / "content"
|
|
124
|
+
staged.write_bytes(content)
|
|
125
|
+
if overwrite:
|
|
126
|
+
os.replace(staged, target)
|
|
127
|
+
else:
|
|
128
|
+
os.link(staged, target)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[
|
|
2
|
+
"**/node_modules/**",
|
|
3
|
+
".git",
|
|
4
|
+
"**/.vscode/**",
|
|
5
|
+
"**/.idea/**",
|
|
6
|
+
"**/*.log",
|
|
7
|
+
"**/.env",
|
|
8
|
+
"**/*.lock",
|
|
9
|
+
"**/.venv/**",
|
|
10
|
+
"**/venv/**",
|
|
11
|
+
"**/env/**",
|
|
12
|
+
"**/__pycache__/**",
|
|
13
|
+
"**/*.pyc",
|
|
14
|
+
"**/*.egg-info/**",
|
|
15
|
+
"**/build/**",
|
|
16
|
+
"**/dist/**",
|
|
17
|
+
"**/.pytest_cache/**",
|
|
18
|
+
"**/.npm/**",
|
|
19
|
+
"**/pnpm-lock.yaml",
|
|
20
|
+
"**/package-lock.json",
|
|
21
|
+
"**/.next/**",
|
|
22
|
+
"**/.DS_Store",
|
|
23
|
+
"**/Thumbs.db",
|
|
24
|
+
"**/*.png",
|
|
25
|
+
"**/*.jpg",
|
|
26
|
+
"**/*.jpeg",
|
|
27
|
+
"**/*.gif",
|
|
28
|
+
"**/*.ico",
|
|
29
|
+
"**/*.svg",
|
|
30
|
+
"**/*.webp",
|
|
31
|
+
"**/*.mp3",
|
|
32
|
+
"**/*.wav",
|
|
33
|
+
"**/*.flac",
|
|
34
|
+
"**/*.mp4",
|
|
35
|
+
"**/*.mov",
|
|
36
|
+
"**/*.avi",
|
|
37
|
+
"**/*.zip",
|
|
38
|
+
"**/*.tar.gz",
|
|
39
|
+
"**/*.rar",
|
|
40
|
+
"**/*.pdf",
|
|
41
|
+
"**/*.doc",
|
|
42
|
+
"**/*.docx",
|
|
43
|
+
"**/*.xls",
|
|
44
|
+
"**/*.xlsx",
|
|
45
|
+
"**/*.dll",
|
|
46
|
+
"**/*.exe",
|
|
47
|
+
"**/*.so",
|
|
48
|
+
"**/*.a",
|
|
49
|
+
"**/*.lib",
|
|
50
|
+
"**/*.o",
|
|
51
|
+
"**/*.bin",
|
|
52
|
+
"**/*.iso"
|
|
53
|
+
]
|