evolver-tools 1.4.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.
Files changed (69) hide show
  1. evolver_tools/__init__.py +2 -0
  2. evolver_tools/__main__.py +3 -0
  3. evolver_tools/cli.py +89 -0
  4. evolver_tools/vendor/b64/__init__.py +2 -0
  5. evolver_tools/vendor/b64/b64.py +176 -0
  6. evolver_tools/vendor/cal_tool/__init__.py +1 -0
  7. evolver_tools/vendor/cal_tool/cli.py +234 -0
  8. evolver_tools/vendor/chart_cli/__init__.py +444 -0
  9. evolver_tools/vendor/chart_cli/__main__.py +3 -0
  10. evolver_tools/vendor/colors/__init__.py +5 -0
  11. evolver_tools/vendor/colors/__main__.py +97 -0
  12. evolver_tools/vendor/csv_stats/__init__.py +5 -0
  13. evolver_tools/vendor/csv_stats/__main__.py +4 -0
  14. evolver_tools/vendor/csv_stats/analyzer.py +258 -0
  15. evolver_tools/vendor/csv_stats/cli.py +45 -0
  16. evolver_tools/vendor/dirsize/__init__.py +183 -0
  17. evolver_tools/vendor/envcheck/__init__.py +426 -0
  18. evolver_tools/vendor/ff/__init__.py +427 -0
  19. evolver_tools/vendor/ff/__main__.py +3 -0
  20. evolver_tools/vendor/find_dups/__init__.py +7 -0
  21. evolver_tools/vendor/find_dups/cli.py +392 -0
  22. evolver_tools/vendor/hashsum/__init__.py +211 -0
  23. evolver_tools/vendor/hashsum/__main__.py +5 -0
  24. evolver_tools/vendor/http_live/__init__.py +265 -0
  25. evolver_tools/vendor/http_live/__main__.py +2 -0
  26. evolver_tools/vendor/ipinfo/__init__.py +3 -0
  27. evolver_tools/vendor/ipinfo/__main__.py +30 -0
  28. evolver_tools/vendor/jq_lite/__init__.py +257 -0
  29. evolver_tools/vendor/jq_lite/__main__.py +5 -0
  30. evolver_tools/vendor/json2csv/__init__.py +3 -0
  31. evolver_tools/vendor/json2csv/__main__.py +82 -0
  32. evolver_tools/vendor/jsonql/__init__.py +326 -0
  33. evolver_tools/vendor/jsonql/__main__.py +5 -0
  34. evolver_tools/vendor/license_cli/__init__.py +1 -0
  35. evolver_tools/vendor/license_cli/__main__.py +4 -0
  36. evolver_tools/vendor/license_cli/cli.py +289 -0
  37. evolver_tools/vendor/markdown_check/__init__.py +211 -0
  38. evolver_tools/vendor/nb/__init__.py +319 -0
  39. evolver_tools/vendor/nb/__main__.py +3 -0
  40. evolver_tools/vendor/passgen/__init__.py +224 -0
  41. evolver_tools/vendor/portcheck/__init__.py +2 -0
  42. evolver_tools/vendor/portcheck/__main__.py +66 -0
  43. evolver_tools/vendor/project_doctor/__init__.py +412 -0
  44. evolver_tools/vendor/project_doctor/__main__.py +3 -0
  45. evolver_tools/vendor/ren/__init__.py +283 -0
  46. evolver_tools/vendor/ren/__main__.py +3 -0
  47. evolver_tools/vendor/siege_lite/__init__.py +250 -0
  48. evolver_tools/vendor/siege_lite/__main__.py +3 -0
  49. evolver_tools/vendor/smellfinder/__init__.py +376 -0
  50. evolver_tools/vendor/smellfinder/__main__.py +3 -0
  51. evolver_tools/vendor/sqlite_cli/__init__.py +326 -0
  52. evolver_tools/vendor/sqlite_cli/__main__.py +5 -0
  53. evolver_tools/vendor/sysmon/__init__.py +299 -0
  54. evolver_tools/vendor/sysmon/__main__.py +3 -0
  55. evolver_tools/vendor/timer/__init__.py +127 -0
  56. evolver_tools/vendor/treedir/__init__.py +2 -0
  57. evolver_tools/vendor/treedir/__main__.py +128 -0
  58. evolver_tools/vendor/urlparse_tool/__init__.py +3 -0
  59. evolver_tools/vendor/urlparse_tool/cli.py +212 -0
  60. evolver_tools/vendor/web_summary/__init__.py +341 -0
  61. evolver_tools/vendor/web_summary/__main__.py +3 -0
  62. evolver_tools/vendor/wordcount/__init__.py +2 -0
  63. evolver_tools/vendor/wordcount/__main__.py +101 -0
  64. evolver_tools-1.4.0.dist-info/METADATA +107 -0
  65. evolver_tools-1.4.0.dist-info/RECORD +69 -0
  66. evolver_tools-1.4.0.dist-info/WHEEL +5 -0
  67. evolver_tools-1.4.0.dist-info/entry_points.txt +34 -0
  68. evolver_tools-1.4.0.dist-info/licenses/LICENSE +21 -0
  69. evolver_tools-1.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ """markdown-check — Markdown 格式校验器
3
+
4
+ 检查 Markdown 文件中的常见问题:
5
+ · 标题层级跳跃 (h1→h3)
6
+ · 断开的锚点链接
7
+ · 代码块未配对
8
+ · 尾随空白
9
+ · 空列表项
10
+ · 过长的行
11
+
12
+ 用法:
13
+ markdown-check README.md
14
+ markdown-check docs/**/*.md
15
+ markdown-check . # 递归检查所有 .md 文件
16
+
17
+ 输出格式: 文件:行:列 级别 消息
18
+
19
+ 零外部依赖。纯 Python 3.8+。
20
+ """
21
+
22
+ import os
23
+ import re
24
+ import sys
25
+ import glob
26
+
27
+
28
+ # ── 规则引擎 ──────────────────────────────────────────────
29
+
30
+ class MarkdownChecker:
31
+ """对单个 .md 文件执行所有检查规则"""
32
+
33
+ def __init__(self, path):
34
+ self.path = path
35
+ self.lines = []
36
+ self.issues = [] # (line, col, level, msg)
37
+ self._code_fence = None # 当前是否在代码块内
38
+
39
+ def check(self):
40
+ try:
41
+ with open(self.path, "r", encoding="utf-8", errors="replace") as f:
42
+ self.lines = f.readlines()
43
+ except Exception as e:
44
+ self.issues.append((1, 0, "ERROR", f"无法读取文件: {e}"))
45
+ return self
46
+
47
+ self._code_fence = None
48
+ headings_seen = set()
49
+
50
+ for i, raw in enumerate(self.lines):
51
+ ln = i + 1
52
+ line = raw.rstrip("\n").rstrip("\r")
53
+ stripped = line.strip()
54
+
55
+ # ── 跟踪代码块 ──
56
+ if line.startswith("```") or line.startswith("~~~"):
57
+ fence_char = line[:3]
58
+ if self._code_fence is None:
59
+ self._code_fence = fence_char
60
+ elif self._code_fence == fence_char:
61
+ # 检查代码块关闭后是否有多余内容
62
+ rest = line[3:].strip()
63
+ if rest and self._code_fence is None:
64
+ pass # 围栏有语言标注,正常
65
+ self._code_fence = None
66
+ continue
67
+
68
+ # 代码块内跳过 lint
69
+ if self._code_fence:
70
+ continue
71
+
72
+ # ── 规则 1: 尾随空白 ──
73
+ if raw.endswith(" \n") or raw.endswith("\t\n") or raw.endswith(" \r\n"):
74
+ self.issues.append((ln, len(raw.rstrip("\n\r")), "WARN", "尾随空白"))
75
+
76
+ # ── 规则 2: 标题层级跳跃 ──
77
+ heading_match = re.match(r"^(#{1,6})\s+(.+)$", line)
78
+ if heading_match:
79
+ level = len(heading_match.group(1))
80
+ text = heading_match.group(2).strip()
81
+ # 检查 ATX 标题是否以 # 结尾(不规范但常见)
82
+ if text.endswith("#"):
83
+ self.issues.append((ln, len(line) - text[::-1].index("#"), "INFO",
84
+ "标题尾部有多余 #"))
85
+
86
+ # 检查是否有更高级别标题出现过
87
+ if headings_seen:
88
+ max_seen = max(headings_seen)
89
+ if level - max_seen > 1:
90
+ self.issues.append((ln, 0, "WARN",
91
+ f"标题层级跳跃: h{max_seen} → h{level}"))
92
+ headings_seen.add(level)
93
+
94
+ # ── 规则 3: 空列表项 ──
95
+ if re.match(r"^(\s*[-*+]|\s*\d+\.)\s*$", line):
96
+ self.issues.append((ln, 0, "WARN", "空列表项"))
97
+
98
+ # ── 规则 4: 过长行 ──
99
+ if len(line) > 120 and not line.startswith("|"): # 跳过表格
100
+ self.issues.append((ln, 120, "INFO", f"行过长 ({len(line)} > 120 字符)"))
101
+
102
+ # ── 规则 5: 重复的标点 ──
103
+ if re.search(r"[!?.,:;]{3,}", line):
104
+ self.issues.append((ln, 0, "INFO", "重复标点"))
105
+
106
+ # ── 规则 6: 图片缺少 alt 文本 ──
107
+ for match in re.finditer(r"!\[(.*?)\]\(.*?\)", line):
108
+ alt_text = match.group(1).strip()
109
+ if not alt_text:
110
+ self.issues.append((ln, match.start(), "WARN", "图片缺少 alt 文本"))
111
+
112
+ # ── 文件级检查 ──
113
+ # 代码块未关闭
114
+ if self._code_fence:
115
+ self.issues.append((len(self.lines), 0, "ERROR", "代码块未关闭"))
116
+
117
+ # 没有标题
118
+ if not headings_seen and self.lines:
119
+ self.issues.append((1, 0, "INFO", "文件没有标题"))
120
+
121
+ # 空文件
122
+ if not self.lines or all(l.strip() == "" for l in self.lines):
123
+ self.issues.append((1, 0, "WARN", "空文件"))
124
+
125
+ return self
126
+
127
+ def report(self, show_all=False):
128
+ """返回格式化报告"""
129
+ total = {"ERROR": 0, "WARN": 0, "INFO": 0}
130
+ lines = []
131
+ for ln, col, level, msg in self.issues:
132
+ total[level] = total.get(level, 0) + 1
133
+ if level == "INFO" and not show_all:
134
+ continue
135
+ col_str = f":{col}" if col else ""
136
+ lines.append(f" {self.path}:{ln}{col_str} {level:<5} {msg}")
137
+
138
+ if not lines:
139
+ return " ✓ 整洁\n"
140
+
141
+ header = (f" {self.path}: "
142
+ f"{total.get('ERROR', 0)} error(s), "
143
+ f"{total.get('WARN', 0)} warn(s), "
144
+ f"{total.get('INFO', 0)} info(s)")
145
+ return header + "\n" + "\n".join(lines) + "\n"
146
+
147
+
148
+ # ── CLI 入口 ──────────────────────────────────────────────
149
+
150
+ def main():
151
+ import argparse
152
+
153
+ parser = argparse.ArgumentParser(
154
+ description="markdown-check — Markdown 格式校验器",
155
+ formatter_class=argparse.RawDescriptionHelpFormatter,
156
+ epilog="示例:\n markdown-check README.md\n markdown-check docs/\n markdown-check -a . # 显示所有信息级提示",
157
+ )
158
+ parser.add_argument("target", nargs="?", default=".", help="文件或目录(默认: 当前目录)")
159
+ parser.add_argument("-a", "--all", action="store_true", help="显示所有信息级提示(含 INFO)")
160
+ parser.add_argument("-q", "--quiet", action="store_true", help="只显示有问题的文件")
161
+ parser.add_argument("--no-recursive", action="store_true", help="不递归搜索子目录")
162
+
163
+ args = parser.parse_args()
164
+ target = args.target
165
+
166
+ # 收集文件
167
+ files = []
168
+ if os.path.isfile(target):
169
+ if target.endswith(".md") or target.endswith(".markdown"):
170
+ files = [target]
171
+ else:
172
+ print(f"错误: 不是 Markdown 文件: {target}", file=sys.stderr)
173
+ sys.exit(1)
174
+ elif os.path.isdir(target):
175
+ pattern = "**/*.md" if not args.no_recursive else "*.md"
176
+ files = sorted(glob.glob(os.path.join(target, pattern), recursive=True))
177
+ else:
178
+ # 尝试 glob
179
+ matches = glob.glob(target, recursive=True)
180
+ files = sorted(f for f in matches if f.endswith((".md", ".markdown")))
181
+
182
+ if not files:
183
+ print("未找到 .md 文件", file=sys.stderr)
184
+ sys.exit(0)
185
+
186
+ # 执行检查
187
+ total_errors = 0
188
+ total_warns = 0
189
+ checked = 0
190
+
191
+ for fpath in files:
192
+ checker = MarkdownChecker(fpath)
193
+ checker.check()
194
+ if not args.quiet or any(i[1] != "INFO" or args.all for i in checker.issues):
195
+ print(checker.report(show_all=args.all))
196
+ for _, _, level, _ in checker.issues:
197
+ if level == "ERROR":
198
+ total_errors += 1
199
+ elif level == "WARN":
200
+ total_warns += 1
201
+ checked += 1
202
+
203
+ # 汇总
204
+ print(f"---\n检查了 {checked} 个文件,"
205
+ f"{total_errors} 个错误,{total_warns} 个警告")
206
+
207
+ sys.exit(1 if total_errors > 0 else 0)
208
+
209
+
210
+ if __name__ == "__main__":
211
+ main()
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ nb — 命令行笔记簿
4
+ 终端下的轻量笔记管理工具。
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ import time
11
+ import re
12
+ from pathlib import Path
13
+ from datetime import datetime
14
+
15
+ NOTES_DIR = Path.home() / '.nb'
16
+ NOTES_FILE = NOTES_DIR / 'notes.json'
17
+ NOTES_EXPORT = NOTES_DIR / 'export'
18
+
19
+ def ensure_db():
20
+ NOTES_DIR.mkdir(parents=True, exist_ok=True)
21
+ if not NOTES_FILE.exists():
22
+ with open(NOTES_FILE, 'w') as f:
23
+ json.dump([], f)
24
+
25
+ def load_notes():
26
+ ensure_db()
27
+ try:
28
+ with open(NOTES_FILE) as f:
29
+ return json.load(f)
30
+ except (json.JSONDecodeError, FileNotFoundError):
31
+ return []
32
+
33
+ def save_notes(notes):
34
+ ensure_db()
35
+ with open(NOTES_FILE, 'w') as f:
36
+ json.dump(notes, f, indent=2, ensure_ascii=False)
37
+
38
+ def next_id(notes):
39
+ if not notes:
40
+ return 1
41
+ return max(n['id'] for n in notes) + 1
42
+
43
+ def timestamp():
44
+ return datetime.now().strftime('%Y-%m-%d %H:%M')
45
+
46
+ def cmd_new(args):
47
+ """nb new <title> — 创建新笔记(从 stdin 读取内容或交互输入)"""
48
+ title = ' '.join(args) if args else '未命名笔记'
49
+
50
+ print(f"📝 标题: {title}")
51
+ print("输入内容(空行 + Ctrl+D 结束):")
52
+ lines = []
53
+ try:
54
+ while True:
55
+ line = sys.stdin.readline()
56
+ if not line:
57
+ break
58
+ line = line.rstrip('\n')
59
+ if line == '' and lines and lines[-1] == '':
60
+ break
61
+ lines.append(line)
62
+ except (EOFError, KeyboardInterrupt):
63
+ pass
64
+
65
+ content = '\n'.join(lines).strip()
66
+ if not content:
67
+ print("✗ 笔记内容为空,取消")
68
+ return
69
+
70
+ notes = load_notes()
71
+ note = {
72
+ 'id': next_id(notes),
73
+ 'title': title,
74
+ 'content': content,
75
+ 'created': timestamp(),
76
+ 'updated': timestamp(),
77
+ }
78
+ notes.insert(0, note) # newest first
79
+ save_notes(notes)
80
+ print(f"✓ 笔记 #{note['id']} 已保存: {title}")
81
+
82
+
83
+ def cmd_list(args):
84
+ """nb list [--all] — 列出笔记"""
85
+ notes = load_notes()
86
+ if not notes:
87
+ print("(没有笔记)")
88
+ return
89
+
90
+ show_all = '--all' in args
91
+ display = notes if show_all else notes[:20]
92
+
93
+ for n in display:
94
+ created = n.get('created', '?')
95
+ title = n['title']
96
+ preview = n['content'][:60].replace('\n', ' ')
97
+ if len(preview) < len(n['content']):
98
+ preview += '...'
99
+ print(f" [{n['id']:>3}] {created} {title}")
100
+ print(f" {preview}")
101
+ print()
102
+
103
+ if not show_all and len(notes) > 20:
104
+ print(f"... 还有 {len(notes) - 20} 条 (使用 --all 查看全部)")
105
+
106
+
107
+ def cmd_show(args):
108
+ """nb show <id> — 显示笔记详情"""
109
+ if not args:
110
+ print("用法: nb show <id>")
111
+ return
112
+ try:
113
+ nid = int(args[0])
114
+ except ValueError:
115
+ print("无效的 ID")
116
+ return
117
+
118
+ notes = load_notes()
119
+ for n in notes:
120
+ if n['id'] == nid:
121
+ print(f"╔══════════════════════════════════════╗")
122
+ print(f"║ #{n['id']} {n['title']}")
123
+ print(f"║ 创建: {n.get('created', '?')}")
124
+ print(f"║ 更新: {n.get('updated', '?')}")
125
+ print(f"╚══════════════════════════════════════╝")
126
+ print()
127
+ print(n['content'])
128
+ return
129
+ print(f"✗ 未找到笔记 #{nid}")
130
+
131
+
132
+ def cmd_search(args):
133
+ """nb search <query> — 搜索笔记"""
134
+ if not args:
135
+ print("用法: nb search <query>")
136
+ return
137
+ query = ' '.join(args).lower()
138
+
139
+ notes = load_notes()
140
+ results = []
141
+ for n in notes:
142
+ if query in n['title'].lower() or query in n['content'].lower():
143
+ results.append(n)
144
+
145
+ if not results:
146
+ print(f"(未找到匹配 '{query}' 的笔记)")
147
+ return
148
+
149
+ print(f"找到 {len(results)} 条匹配 '{query}':\n")
150
+ for n in results:
151
+ # Highlight matches
152
+ title = n['title']
153
+ preview = n['content'][:80].replace('\n', ' ')
154
+ print(f" [{n['id']:>3}] {n.get('created', '?')} {title}")
155
+ print(f" {preview}")
156
+ print()
157
+
158
+
159
+ def cmd_delete(args):
160
+ """nb delete <id> — 删除笔记"""
161
+ if not args:
162
+ print("用法: nb delete <id>")
163
+ return
164
+ try:
165
+ nid = int(args[0])
166
+ except ValueError:
167
+ print("无效的 ID")
168
+ return
169
+
170
+ notes = load_notes()
171
+ for i, n in enumerate(notes):
172
+ if n['id'] == nid:
173
+ confirm = input(f"删除笔记 #{nid} 「{n['title']}」? (y/N) ").lower()
174
+ if confirm == 'y':
175
+ notes.pop(i)
176
+ save_notes(notes)
177
+ print(f"✓ 笔记 #{nid} 已删除")
178
+ else:
179
+ print("取消")
180
+ return
181
+ print(f"✗ 未找到笔记 #{nid}")
182
+
183
+
184
+ def cmd_edit(args):
185
+ """nb edit <id> — 编辑笔记内容(替换内容)"""
186
+ if not args:
187
+ print("用法: nb edit <id>")
188
+ return
189
+ try:
190
+ nid = int(args[0])
191
+ except ValueError:
192
+ print("无效的 ID")
193
+ return
194
+
195
+ notes = load_notes()
196
+ for n in notes:
197
+ if n['id'] == nid:
198
+ print(f"编辑笔记 #{nid} 「{n['title']}」")
199
+ print("当前内容:")
200
+ print(n['content'])
201
+ print("\n--- 输入新内容(空行 + Ctrl+D 结束)---")
202
+ lines = []
203
+ try:
204
+ while True:
205
+ line = sys.stdin.readline()
206
+ if not line:
207
+ break
208
+ line = line.rstrip('\n')
209
+ if line == '' and lines and lines[-1] == '':
210
+ break
211
+ lines.append(line)
212
+ except (EOFError, KeyboardInterrupt):
213
+ pass
214
+ content = '\n'.join(lines).strip()
215
+ if content:
216
+ n['content'] = content
217
+ n['updated'] = timestamp()
218
+ save_notes(notes)
219
+ print(f"✓ 笔记 #{nid} 已更新")
220
+ else:
221
+ print("内容为空,未修改")
222
+ return
223
+ print(f"✗ 未找到笔记 #{nid}")
224
+
225
+
226
+ def cmd_export(args):
227
+ """nb export — 导出所有笔记为 Markdown 文件"""
228
+ notes = load_notes()
229
+ if not notes:
230
+ print("(没有笔记可导出)")
231
+ return
232
+
233
+ export_dir = NOTES_EXPORT
234
+ export_dir.mkdir(parents=True, exist_ok=True)
235
+
236
+ # Also clear old exports
237
+ for f in export_dir.glob('*.md'):
238
+ f.unlink()
239
+
240
+ for n in notes:
241
+ safe_title = re.sub(r'[^\w\s-]', '', n['title']).strip()[:40]
242
+ safe_title = re.sub(r'[-\s]+', '-', safe_title).lower()
243
+ if not safe_title:
244
+ safe_title = f'note-{n["id"]}'
245
+ filename = f"{n['id']:03d}-{safe_title}.md"
246
+
247
+ md = f"# {n['title']}\n\n"
248
+ md += f"- **ID**: #{n['id']}\n"
249
+ md += f"- **创建**: {n.get('created', '?')}\n"
250
+ md += f"- **更新**: {n.get('updated', '?')}\n\n"
251
+ md += "---\n\n"
252
+ md += n['content'] + '\n'
253
+
254
+ with open(export_dir / filename, 'w') as f:
255
+ f.write(md)
256
+
257
+ print(f"✓ 已导出 {len(notes)} 条笔记到 {export_dir}")
258
+
259
+
260
+ def cmd_count(args):
261
+ """nb count — 统计笔记数量"""
262
+ notes = load_notes()
263
+ total = len(notes)
264
+ total_chars = sum(len(n['content']) for n in notes)
265
+ print(f"📊 笔记数: {total}")
266
+ print(f"📊 总字数: {total_chars}")
267
+
268
+
269
+ def show_help():
270
+ print("nb — 命令行笔记簿")
271
+ print()
272
+ print("用法:")
273
+ print(" nb new <title> 创建新笔记")
274
+ print(" nb list [--all] 列出笔记")
275
+ print(" nb show <id> 查看笔记")
276
+ print(" nb search <query> 搜索笔记")
277
+ print(" nb edit <id> 编辑笔记")
278
+ print(" nb delete <id> 删除笔记")
279
+ print(" nb export 导出 Markdown")
280
+ print(" nb count 统计信息")
281
+ print(" nb help 显示帮助")
282
+ print()
283
+ print("创建笔记时,输入内容后 Ctrl+D 结束。")
284
+
285
+
286
+ def main():
287
+ if len(sys.argv) < 2 or sys.argv[1] in ('help', '--help', '-h'):
288
+ show_help()
289
+ return
290
+
291
+ cmd = sys.argv[1]
292
+ args = sys.argv[2:]
293
+
294
+ commands = {
295
+ 'new': cmd_new,
296
+ 'list': cmd_list,
297
+ 'ls': cmd_list,
298
+ 'show': cmd_show,
299
+ 'get': cmd_show,
300
+ 'search': cmd_search,
301
+ 'find': cmd_search,
302
+ 'edit': cmd_edit,
303
+ 'delete': cmd_delete,
304
+ 'rm': cmd_delete,
305
+ 'export': cmd_export,
306
+ 'count': cmd_count,
307
+ 'stats': cmd_count,
308
+ }
309
+
310
+ if cmd in commands:
311
+ ensure_db()
312
+ commands[cmd](args)
313
+ else:
314
+ print(f"未知命令: {cmd}")
315
+ print("使用 'nb help' 查看帮助")
316
+
317
+
318
+ if __name__ == '__main__':
319
+ main()
@@ -0,0 +1,3 @@
1
+ """CLI entry point for `python -m nb`"""
2
+ from nb import main
3
+ main()