cheatcheat 0.1.0__py3-none-any.whl
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.
cheatcheat/__init__.py
ADDED
|
File without changes
|
cheatcheat/main.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import glob
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import yaml
|
|
12
|
+
except ImportError:
|
|
13
|
+
print("Error: PyYAML is not installed. Please install it using 'pip install PyYAML'.")
|
|
14
|
+
sys.exit(1)
|
|
15
|
+
|
|
16
|
+
def load_config(config_path):
|
|
17
|
+
"""Loads configuration from yaml file."""
|
|
18
|
+
if not os.path.exists(config_path):
|
|
19
|
+
print(f"Configuration file not found: {config_path}")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
with open(config_path, 'r') as f:
|
|
23
|
+
try:
|
|
24
|
+
return yaml.safe_load(f)
|
|
25
|
+
except yaml.YAMLError as e:
|
|
26
|
+
print(f"Error parsing config file: {e}")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
def get_cheatpaths(config):
|
|
30
|
+
"""
|
|
31
|
+
Returns a list of cheatpaths from config, ensuring local .cheat dirs are included.
|
|
32
|
+
Returns list of dicts: {'name': str, 'path': str, 'readonly': bool, 'tags': list}
|
|
33
|
+
"""
|
|
34
|
+
paths = []
|
|
35
|
+
|
|
36
|
+
# 1. Configured paths
|
|
37
|
+
if 'cheatpaths' in config:
|
|
38
|
+
for cp in config['cheatpaths']:
|
|
39
|
+
expanded_path = os.path.expanduser(cp['path'])
|
|
40
|
+
path_entry = cp.copy()
|
|
41
|
+
path_entry['path'] = expanded_path
|
|
42
|
+
paths.append(path_entry)
|
|
43
|
+
|
|
44
|
+
# 2. Local .cheat directory (highest priority, so maybe prepend? README says "append ... to the cheatpath"
|
|
45
|
+
# but also "will be treated as the most local ... and will override less local".
|
|
46
|
+
# Usually "override" means it is checked *first*.
|
|
47
|
+
# The README says: "The most global cheatpath is listed first in this file; the most local is listed last."
|
|
48
|
+
# "For example, if there is a 'tar' cheatsheet on both global and local paths, you'll be presented with the local one by default."
|
|
49
|
+
# So we should iterate in reverse order to find the "winner", or iterate in priority order (local first) to find the first match.
|
|
50
|
+
# Let's construct a list where the *last* element has the *highest* priority, matching `conf.yml` structure description.
|
|
51
|
+
|
|
52
|
+
local_cheat = os.path.join(os.getcwd(), '.cheat')
|
|
53
|
+
if os.path.isdir(local_cheat):
|
|
54
|
+
paths.append({
|
|
55
|
+
'name': 'local',
|
|
56
|
+
'path': local_cheat,
|
|
57
|
+
'readonly': False,
|
|
58
|
+
'tags': ['local']
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return paths
|
|
62
|
+
|
|
63
|
+
def find_cheatsheet(cheatname, paths):
|
|
64
|
+
"""
|
|
65
|
+
Finds a cheatsheet by name.
|
|
66
|
+
Searches from most local (last in list) to most global (first in list).
|
|
67
|
+
Returns (path_entry, full_path) or (None, None).
|
|
68
|
+
"""
|
|
69
|
+
# Iterate backwards (most local first)
|
|
70
|
+
for path_entry in reversed(paths):
|
|
71
|
+
base_dir = path_entry['path']
|
|
72
|
+
# Check for exact match or match with extension
|
|
73
|
+
# We need to handle subdirectories too, e.g. cheatname = "foo/bar"
|
|
74
|
+
|
|
75
|
+
# Security check: prevent breakout
|
|
76
|
+
target_path = os.path.join(base_dir, cheatname)
|
|
77
|
+
if not os.path.abspath(target_path).startswith(os.path.abspath(base_dir)):
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# Check file exact
|
|
81
|
+
if os.path.isfile(target_path):
|
|
82
|
+
return path_entry, target_path
|
|
83
|
+
|
|
84
|
+
# Check file with any extension? README says "file is named 'bar' or 'bar.EXTENSION'"
|
|
85
|
+
# glob for target_path.*
|
|
86
|
+
matches = glob.glob(target_path + ".*")
|
|
87
|
+
if matches:
|
|
88
|
+
# Pick the first one?
|
|
89
|
+
return path_entry, matches[0]
|
|
90
|
+
|
|
91
|
+
return None, None
|
|
92
|
+
|
|
93
|
+
def list_cheatsheets(paths, filter_path_name=None):
|
|
94
|
+
"""Lists all cheatsheets."""
|
|
95
|
+
sheets = set()
|
|
96
|
+
|
|
97
|
+
# Iterate all paths
|
|
98
|
+
for entry in paths:
|
|
99
|
+
if filter_path_name and entry.get('name') != filter_path_name:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
base_dir = entry['path']
|
|
103
|
+
if not os.path.isdir(base_dir):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
# Walk directory
|
|
107
|
+
for root, dirs, files in os.walk(base_dir):
|
|
108
|
+
for file in files:
|
|
109
|
+
if file.startswith('.'): continue # ignore hidden files
|
|
110
|
+
|
|
111
|
+
# Rel path from base_dir
|
|
112
|
+
abs_path = os.path.join(root, file)
|
|
113
|
+
rel_path = os.path.relpath(abs_path, base_dir)
|
|
114
|
+
|
|
115
|
+
# Remove extension for display?
|
|
116
|
+
# README says: named "tar" or "tar.EXTENSION".
|
|
117
|
+
# If we have tar.md, we display tar.
|
|
118
|
+
base, ext = os.path.splitext(rel_path)
|
|
119
|
+
# If there are multiple extensions or weird dots, this might be tricky.
|
|
120
|
+
# But simple logic: if it's a file, it's a cheat.
|
|
121
|
+
|
|
122
|
+
# Check if it matches the pattern
|
|
123
|
+
sheets.add(base)
|
|
124
|
+
|
|
125
|
+
# Sort and print
|
|
126
|
+
for sheet in sorted(sheets):
|
|
127
|
+
print(sheet)
|
|
128
|
+
|
|
129
|
+
def search_cheatsheets(term, paths):
|
|
130
|
+
"""Searches for term in all cheatsheets."""
|
|
131
|
+
found = False
|
|
132
|
+
# Walk all paths
|
|
133
|
+
for entry in paths:
|
|
134
|
+
base_dir = entry['path']
|
|
135
|
+
if not os.path.isdir(base_dir):
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
for root, dirs, files in os.walk(base_dir):
|
|
139
|
+
for file in files:
|
|
140
|
+
if file.startswith('.'): continue
|
|
141
|
+
abs_path = os.path.join(root, file)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
with open(abs_path, 'r', errors='ignore') as f:
|
|
145
|
+
lines = f.readlines()
|
|
146
|
+
for i, line in enumerate(lines):
|
|
147
|
+
if term.lower() in line.lower():
|
|
148
|
+
# Calculate sheet name
|
|
149
|
+
rel_path = os.path.relpath(abs_path, base_dir)
|
|
150
|
+
sheet_name, _ = os.path.splitext(rel_path)
|
|
151
|
+
print(f"{sheet_name}:{i+1}: {line.strip()}")
|
|
152
|
+
found = True
|
|
153
|
+
except Exception as e:
|
|
154
|
+
# Ignore read errors
|
|
155
|
+
pass
|
|
156
|
+
return found
|
|
157
|
+
|
|
158
|
+
def edit_cheatsheet(cheatname, paths, config):
|
|
159
|
+
"""Edits a cheatsheet. Handles copy-on-write."""
|
|
160
|
+
# check if it exists
|
|
161
|
+
path_entry, full_path = find_cheatsheet(cheatname, paths)
|
|
162
|
+
|
|
163
|
+
editor = config.get('editor', os.environ.get('EDITOR', 'vi'))
|
|
164
|
+
|
|
165
|
+
if path_entry:
|
|
166
|
+
# Exists
|
|
167
|
+
if not path_entry.get('readonly', False):
|
|
168
|
+
# Writable, just open
|
|
169
|
+
subprocess.call(f"{editor} '{full_path}'", shell=True)
|
|
170
|
+
else:
|
|
171
|
+
# Read-only, need to copy to first writable path
|
|
172
|
+
print(f"Path '{path_entry['name']}' is read-only. Copying to personal path...")
|
|
173
|
+
|
|
174
|
+
# Find last writable path (most local)
|
|
175
|
+
target_entry = None
|
|
176
|
+
for entry in reversed(paths): # Iterate in reverse priority order (Local -> Global)
|
|
177
|
+
if not entry.get('readonly', False):
|
|
178
|
+
target_entry = entry
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
if not target_entry:
|
|
182
|
+
print("Error: No writable cheatpath found.")
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
|
|
185
|
+
# Construct new path
|
|
186
|
+
# We assume cheatsheet name might have subdirs
|
|
187
|
+
new_full_path = os.path.join(target_entry['path'], cheatname)
|
|
188
|
+
|
|
189
|
+
# If extension was present in original, try to preserve it?
|
|
190
|
+
# get extension from full_path
|
|
191
|
+
_, ext = os.path.splitext(full_path)
|
|
192
|
+
if ext and not new_full_path.endswith(ext):
|
|
193
|
+
new_full_path += ext
|
|
194
|
+
|
|
195
|
+
# Ensure dir exists
|
|
196
|
+
os.makedirs(os.path.dirname(new_full_path), exist_ok=True)
|
|
197
|
+
|
|
198
|
+
# Copy
|
|
199
|
+
shutil.copy2(full_path, new_full_path)
|
|
200
|
+
print(f"Copied to {new_full_path}")
|
|
201
|
+
|
|
202
|
+
# Open
|
|
203
|
+
subprocess.call(f"{editor} '{new_full_path}'", shell=True)
|
|
204
|
+
|
|
205
|
+
else:
|
|
206
|
+
# Does not exist. Create new.
|
|
207
|
+
# Find last writable path (most local)
|
|
208
|
+
target_entry = None
|
|
209
|
+
for entry in reversed(paths):
|
|
210
|
+
if not entry.get('readonly', False):
|
|
211
|
+
target_entry = entry
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
if not target_entry:
|
|
215
|
+
print("Error: No writable cheatpath found.")
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
|
|
218
|
+
new_full_path = os.path.join(target_entry['path'], cheatname)
|
|
219
|
+
# Verify if we should add extension? For now default to no extension or just as is.
|
|
220
|
+
|
|
221
|
+
os.makedirs(os.path.dirname(new_full_path), exist_ok=True)
|
|
222
|
+
# Open editor (editor will create file usually, or we touch it)
|
|
223
|
+
# subprocess.call will open it.
|
|
224
|
+
subprocess.call(f"{editor} '{new_full_path}'", shell=True)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def view_cheatsheet(cheatname, paths, config):
|
|
228
|
+
"""View a cheatsheet."""
|
|
229
|
+
path_entry, full_path = find_cheatsheet(cheatname, paths)
|
|
230
|
+
|
|
231
|
+
if not path_entry:
|
|
232
|
+
print(f"No cheatsheet found for '{cheatname}'.")
|
|
233
|
+
# Optional: Print "Did you mean...?"
|
|
234
|
+
sys.exit(1)
|
|
235
|
+
|
|
236
|
+
viewer = config.get('viewer', os.environ.get('PAGER', 'less'))
|
|
237
|
+
|
|
238
|
+
# Run viewer
|
|
239
|
+
try:
|
|
240
|
+
subprocess.call(f"{viewer} '{full_path}'", shell=True)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
print(f"Error opening viewer: {e}")
|
|
243
|
+
# Fallback to cat
|
|
244
|
+
with open(full_path, 'r') as f:
|
|
245
|
+
print(f.read())
|
|
246
|
+
|
|
247
|
+
def main():
|
|
248
|
+
parser = argparse.ArgumentParser(description="Create and view interactive cheatsheets.")
|
|
249
|
+
parser.add_argument("cheatname", nargs="?", help="The name of the cheatsheet to view/edit.")
|
|
250
|
+
parser.add_argument("-e", "--edit", action="store_true", help="Edit a cheatsheet.")
|
|
251
|
+
parser.add_argument("-l", "--list", action="store_true", help="List all available cheatsheets.")
|
|
252
|
+
parser.add_argument("-p", "--path", help="Filter by cheatpath name (used with -l).")
|
|
253
|
+
parser.add_argument("-s", "--search", help="Search for a term among cheatsheets.")
|
|
254
|
+
parser.add_argument("-d", "--directories", action="store_true", help="List configured cheatpaths.")
|
|
255
|
+
parser.add_argument("--conf", default=os.path.expanduser("~/.config/cheat/conf.yml"), help="Path to config file.")
|
|
256
|
+
|
|
257
|
+
# We might need to handle the case where conf.yml is in current dir or specific location?
|
|
258
|
+
# User env: /Users/cche/git/ch/conf.yml. I will default to look there for this task if not specified?
|
|
259
|
+
# Or just rely on the argument.
|
|
260
|
+
# For this specific task, the User provided @[conf.yml], so it's likely expected to work with that or default user config.
|
|
261
|
+
# I'll default to looking in local dir first for dev purposes if not found in standard locations.
|
|
262
|
+
|
|
263
|
+
args = parser.parse_args()
|
|
264
|
+
|
|
265
|
+
# Config resolution
|
|
266
|
+
config_path = args.conf
|
|
267
|
+
# Fallback to local 'conf.yml' if default doesn't exist but local does (for dev context)
|
|
268
|
+
if config_path == os.path.expanduser("~/.config/cheat/conf.yml") and not os.path.exists(config_path):
|
|
269
|
+
if os.path.exists("conf.yml"):
|
|
270
|
+
config_path = "conf.yml"
|
|
271
|
+
|
|
272
|
+
config = load_config(config_path)
|
|
273
|
+
paths = get_cheatpaths(config)
|
|
274
|
+
|
|
275
|
+
if args.directories:
|
|
276
|
+
for p in paths:
|
|
277
|
+
print(f"{p['name']}: {p['path']} (readonly: {p.get('readonly', False)})")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
if args.list:
|
|
281
|
+
list_cheatsheets(paths, args.path)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
if args.search:
|
|
285
|
+
search_cheatsheets(args.search, paths)
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
if args.edit:
|
|
289
|
+
if not args.cheatname:
|
|
290
|
+
print("Error: Please specify a cheatsheet to edit.")
|
|
291
|
+
sys.exit(1)
|
|
292
|
+
edit_cheatsheet(args.cheatname, paths, config)
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
if args.cheatname:
|
|
296
|
+
view_cheatsheet(args.cheatname, paths, config)
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# If no args, print help
|
|
300
|
+
parser.print_help()
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
main()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cheatcheat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Create and view interactive cheatsheets on the command-line
|
|
5
|
+
Author-email: ChongChong He <chongchonghe@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/chongchonghe/cheatcheat
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/chongchonghe/cheatcheat/issues
|
|
9
|
+
Keywords: cheat,cheatsheet,cli
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Utilities
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: PyYAML
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
cheat
|
|
21
|
+
=====
|
|
22
|
+
|
|
23
|
+
`cheat` allows you to create and view interactive cheatsheets on the
|
|
24
|
+
command-line. It was designed to help remind \*nix system administrators of
|
|
25
|
+
options for commands that they use frequently, but not frequently enough to
|
|
26
|
+
remember.
|
|
27
|
+
|
|
28
|
+
Example
|
|
29
|
+
-------
|
|
30
|
+
The next time you're forced to disarm a nuclear weapon without consulting
|
|
31
|
+
Google, you may run:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
cheat tar
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
You will be presented with a cheatsheet resembling the following:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
# To extract an uncompressed archive:
|
|
41
|
+
tar -xvf '/path/to/foo.tar'
|
|
42
|
+
|
|
43
|
+
# To extract a .gz archive:
|
|
44
|
+
tar -xzvf '/path/to/foo.tgz'
|
|
45
|
+
|
|
46
|
+
# To create a .gz archive:
|
|
47
|
+
tar -czvf '/path/to/foo.tgz' '/path/to/foo/'
|
|
48
|
+
|
|
49
|
+
# To extract a .bz2 archive:
|
|
50
|
+
tar -xjvf '/path/to/foo.tgz'
|
|
51
|
+
|
|
52
|
+
# To create a .bz2 archive:
|
|
53
|
+
tar -cjvf '/path/to/foo.tgz' '/path/to/foo/'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Usage
|
|
57
|
+
-----
|
|
58
|
+
To view a cheatsheet:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
cheat tar # a "top-level" cheatsheet
|
|
62
|
+
cheat foo/bar # a "nested" cheatsheet
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
To edit a cheatsheet:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
cheat -e tar # opens the "tar" cheatsheet for editing, or creates it if it does not exist
|
|
69
|
+
cheat -e foo/bar # nested cheatsheets are accessed like this
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To view the configured cheatpaths:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
cheat -d
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
To list all available cheatsheets:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
cheat -l
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
To list all cheatsheets on the "personal" path:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
cheat -l -p personal
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
To search for the phrase "ssh" among cheatsheets:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
cheat -s ssh
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Cheatsheets
|
|
97
|
+
-----------
|
|
98
|
+
Cheatsheets are plain-text files with optional file extensions. When the extension is excluded, they are named
|
|
99
|
+
according to the command used to view them:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
cheat tar # file is named "tar" or "tar.EXTENSION"
|
|
103
|
+
cheat foo/bar # file is named "bar" or "bar.EXTENSION", in a "foo" subdirectory
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Cheatpaths
|
|
107
|
+
----------
|
|
108
|
+
Cheatsheets are stored on "cheatpaths", which are directories that contain
|
|
109
|
+
cheatsheets. Cheatpaths are specified in the `conf.yml` file.
|
|
110
|
+
|
|
111
|
+
It can be useful to configure `cheat` against multiple cheatpaths. A common
|
|
112
|
+
pattern is to store cheatsheets from multiple repositories on individual
|
|
113
|
+
cheatpaths:
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
# conf.yml:
|
|
117
|
+
# ...
|
|
118
|
+
cheatpaths:
|
|
119
|
+
- name: community # a name for the cheatpath
|
|
120
|
+
path: ~/documents/cheat/community # the path's location on the filesystem
|
|
121
|
+
tags: [ community ] # these tags will be applied to all sheets on the path
|
|
122
|
+
readonly: true # if true, `cheat` will not create new cheatsheets here
|
|
123
|
+
|
|
124
|
+
- name: personal
|
|
125
|
+
path: ~/documents/cheat/personal # this is a separate directory and repository than above
|
|
126
|
+
tags: [ personal ]
|
|
127
|
+
readonly: false # new sheets may be written here
|
|
128
|
+
# ...
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The `readonly` option instructs `cheat` not to edit (or create) any cheatsheets
|
|
132
|
+
on the path. This is useful to prevent merge-conflicts from arising on upstream
|
|
133
|
+
cheatsheet repositories.
|
|
134
|
+
|
|
135
|
+
If a user attempts to edit a cheatsheet on a read-only cheatpath, `cheat` will
|
|
136
|
+
transparently copy that sheet to the first writeable directory in 'cheatpaths' before opening it for
|
|
137
|
+
editing.
|
|
138
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
cheatcheat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
cheatcheat/main.py,sha256=MslE6blgxVnxjGzZ3mHjpeMAoz71tGK270D_xxFHOAM,11583
|
|
3
|
+
cheatcheat-0.1.0.dist-info/METADATA,sha256=g6KNvbz6bggK9jv71hqSHdnVQkDDCyBSNLQL0aGL0_4,3648
|
|
4
|
+
cheatcheat-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
cheatcheat-0.1.0.dist-info/entry_points.txt,sha256=4LuOywarRBwVxCbpkLTLzkZEb-8fBIvMSlNTsLXbuIc,47
|
|
6
|
+
cheatcheat-0.1.0.dist-info/top_level.txt,sha256=i6HZwer-VZ2knnrjEZFukKETIudfxQtk8dK1i4_rhw4,11
|
|
7
|
+
cheatcheat-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cheatcheat
|