claude-nb 0.4.0 → 0.5.43
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/Makefile +8 -2
- package/README.md +262 -55
- package/VERSION +1 -1
- package/bin/_pip_entry.py +1 -1
- package/bin/board +119 -34
- package/bin/check-changelog +98 -0
- package/bin/check-npm-package +89 -0
- package/bin/check-readme-sync +69 -0
- package/bin/cnb +150 -38
- package/bin/configure-godaddy-pages-dns +203 -0
- package/bin/dispatcher +25 -11
- package/bin/doctor +55 -6
- package/bin/init +70 -9
- package/bin/notify +278 -0
- package/bin/registry +8 -23
- package/bin/secret-scan +165 -0
- package/bin/shutdown +68 -0
- package/bin/sync-version +131 -0
- package/lib/board_admin.py +18 -24
- package/lib/board_bbs.py +28 -19
- package/lib/board_bug.py +11 -22
- package/lib/board_db.py +37 -6
- package/lib/board_lock.py +16 -12
- package/lib/board_mail.py +212 -0
- package/lib/board_mailbox.py +41 -4
- package/lib/board_msg.py +88 -25
- package/lib/board_own.py +288 -0
- package/lib/board_pending.py +236 -0
- package/lib/board_pulse.py +14 -0
- package/lib/board_task.py +45 -17
- package/lib/board_tui.py +37 -23
- package/lib/board_view.py +113 -57
- package/lib/board_vote.py +12 -15
- package/lib/build_lock.py +9 -9
- package/lib/cli.py +3 -3
- package/lib/common.py +67 -7
- package/lib/concerns/__init__.py +4 -1
- package/lib/concerns/coral.py +1 -1
- package/lib/concerns/digest_scheduler.py +128 -0
- package/lib/concerns/file_watcher.py +84 -69
- package/lib/concerns/health.py +1 -1
- package/lib/concerns/helpers.py +17 -37
- package/lib/concerns/notification_push.py +178 -0
- package/lib/concerns/notifications.py +58 -3
- package/lib/concerns/nudge_coordinator.py +148 -0
- package/lib/digest.py +142 -0
- package/lib/github_issues_sync.py +173 -0
- package/lib/global_registry.py +183 -0
- package/lib/health.py +2 -2
- package/lib/inject.py +23 -15
- package/lib/migrate.py +8 -4
- package/lib/monitor.py +33 -13
- package/lib/notification_config.py +121 -0
- package/lib/notification_delivery.py +95 -0
- package/lib/panel.py +6 -2
- package/lib/resources.py +6 -3
- package/lib/shift_report.py +223 -0
- package/lib/shutdown.py +198 -0
- package/lib/swarm.py +43 -35
- package/lib/swarm_backend.py +63 -29
- package/lib/theme_profiles.py +89 -0
- package/lib/tmux_utils.py +52 -0
- package/lib/token_usage.py +148 -0
- package/migrations/004_heartbeat.sql +1 -0
- package/migrations/005_notification_log.sql +12 -0
- package/migrations/006_pending_actions.sql +15 -0
- package/migrations/007_mail.sql +15 -0
- package/migrations/008_ownership.sql +10 -0
- package/package.json +38 -4
- package/pyproject.toml +3 -2
- package/registry/0005-ritchie.json +12 -0
- package/registry/README.md +9 -0
- package/registry/pubkeys.json +83 -3
- package/schema.sql +54 -1
package/bin/board
CHANGED
|
@@ -14,7 +14,7 @@ CLAUDES_HOME = Path(__file__).resolve().parent.parent
|
|
|
14
14
|
sys.path.insert(0, str(CLAUDES_HOME))
|
|
15
15
|
|
|
16
16
|
from lib.board_db import BoardDB
|
|
17
|
-
from lib.common import ClaudesEnv, parse_flags
|
|
17
|
+
from lib.common import ClaudesEnv, parse_flags, validate_identity
|
|
18
18
|
|
|
19
19
|
# ---------------------------------------------------------------------------
|
|
20
20
|
# Command registry
|
|
@@ -33,14 +33,15 @@ class Command:
|
|
|
33
33
|
needs_identity: bool = True
|
|
34
34
|
takes_rest: bool = True
|
|
35
35
|
aliases: list[str] = field(default_factory=list)
|
|
36
|
+
hidden: bool = False
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
COMMANDS: list[Command] = [
|
|
39
40
|
# ── messaging ──
|
|
40
|
-
Command("send", "lib.board_msg", "cmd_send", "send a message", "send <to> <msg> [--attach <f>]"),
|
|
41
|
-
Command("status", "lib.board_msg", "cmd_status", "update your current task", "status <description>"),
|
|
42
|
-
Command("inbox", "lib.board_msg", "cmd_inbox", "check unread messages", "inbox", takes_rest=False),
|
|
43
|
-
Command("ack", "lib.board_msg", "cmd_ack", "clear inbox", "ack", takes_rest=False),
|
|
41
|
+
Command("send", "lib.board_msg", "cmd_send", "send a message", "send <to> <msg> [--attach <f>]", hidden=True),
|
|
42
|
+
Command("status", "lib.board_msg", "cmd_status", "update your current task", "status <description>", hidden=True),
|
|
43
|
+
Command("inbox", "lib.board_msg", "cmd_inbox", "check unread messages", "inbox", takes_rest=False, hidden=True),
|
|
44
|
+
Command("ack", "lib.board_msg", "cmd_ack", "clear inbox", "ack", takes_rest=False, hidden=True),
|
|
44
45
|
Command("log", "lib.board_msg", "cmd_log", "message history", "log [n] [--mine]"),
|
|
45
46
|
# ── views ──
|
|
46
47
|
Command("view", "lib.board_view", "cmd_view", "session dashboard", "view", takes_rest=False),
|
|
@@ -72,6 +73,7 @@ COMMANDS: list[Command] = [
|
|
|
72
73
|
"pre-build",
|
|
73
74
|
needs_identity=False,
|
|
74
75
|
takes_rest=False,
|
|
76
|
+
hidden=True,
|
|
75
77
|
),
|
|
76
78
|
Command(
|
|
77
79
|
"dirty",
|
|
@@ -81,12 +83,37 @@ COMMANDS: list[Command] = [
|
|
|
81
83
|
"dirty",
|
|
82
84
|
needs_identity=False,
|
|
83
85
|
takes_rest=False,
|
|
86
|
+
hidden=True,
|
|
84
87
|
),
|
|
85
88
|
Command(
|
|
86
|
-
"files",
|
|
89
|
+
"files",
|
|
90
|
+
"lib.board_view",
|
|
91
|
+
"cmd_files",
|
|
92
|
+
"list shared files",
|
|
93
|
+
"files",
|
|
94
|
+
needs_identity=False,
|
|
95
|
+
takes_rest=False,
|
|
96
|
+
hidden=True,
|
|
97
|
+
),
|
|
98
|
+
Command(
|
|
99
|
+
"get",
|
|
100
|
+
"lib.board_view",
|
|
101
|
+
"cmd_get",
|
|
102
|
+
"view shared file content",
|
|
103
|
+
"get <hash|name>",
|
|
104
|
+
needs_identity=False,
|
|
105
|
+
hidden=True,
|
|
106
|
+
),
|
|
107
|
+
Command(
|
|
108
|
+
"roster",
|
|
109
|
+
"lib.board_view",
|
|
110
|
+
"cmd_roster",
|
|
111
|
+
"team roster",
|
|
112
|
+
"roster",
|
|
113
|
+
needs_identity=False,
|
|
114
|
+
takes_rest=False,
|
|
115
|
+
hidden=True,
|
|
87
116
|
),
|
|
88
|
-
Command("get", "lib.board_view", "cmd_get", "view shared file content", "get <hash|name>", needs_identity=False),
|
|
89
|
-
Command("roster", "lib.board_view", "cmd_roster", "team roster", "roster", needs_identity=False, takes_rest=False),
|
|
90
117
|
Command(
|
|
91
118
|
"history",
|
|
92
119
|
"lib.board_view",
|
|
@@ -94,6 +121,7 @@ COMMANDS: list[Command] = [
|
|
|
94
121
|
"session message history",
|
|
95
122
|
"history <session> [limit]",
|
|
96
123
|
needs_identity=False,
|
|
124
|
+
hidden=True,
|
|
97
125
|
),
|
|
98
126
|
Command(
|
|
99
127
|
"freshness",
|
|
@@ -103,6 +131,7 @@ COMMANDS: list[Command] = [
|
|
|
103
131
|
"freshness",
|
|
104
132
|
needs_identity=False,
|
|
105
133
|
takes_rest=False,
|
|
134
|
+
hidden=True,
|
|
106
135
|
),
|
|
107
136
|
Command(
|
|
108
137
|
"relations",
|
|
@@ -112,11 +141,20 @@ COMMANDS: list[Command] = [
|
|
|
112
141
|
"relations",
|
|
113
142
|
needs_identity=False,
|
|
114
143
|
takes_rest=False,
|
|
144
|
+
hidden=True,
|
|
115
145
|
),
|
|
116
146
|
# ── BBS ──
|
|
117
|
-
Command("post", "lib.board_bbs", "cmd_post", "BBS: create new thread", "post <标题> <内容>"),
|
|
118
|
-
Command("reply", "lib.board_bbs", "cmd_reply", "BBS: reply to thread", "reply <帖子ID> <内容>"),
|
|
119
|
-
Command(
|
|
147
|
+
Command("post", "lib.board_bbs", "cmd_post", "BBS: create new thread", "post <标题> <内容>", hidden=True),
|
|
148
|
+
Command("reply", "lib.board_bbs", "cmd_reply", "BBS: reply to thread", "reply <帖子ID> <内容>", hidden=True),
|
|
149
|
+
Command(
|
|
150
|
+
"thread",
|
|
151
|
+
"lib.board_bbs",
|
|
152
|
+
"cmd_thread",
|
|
153
|
+
"BBS: view thread",
|
|
154
|
+
"thread <帖子ID>",
|
|
155
|
+
needs_identity=False,
|
|
156
|
+
hidden=True,
|
|
157
|
+
),
|
|
120
158
|
Command(
|
|
121
159
|
"threads",
|
|
122
160
|
"lib.board_bbs",
|
|
@@ -125,19 +163,41 @@ COMMANDS: list[Command] = [
|
|
|
125
163
|
"threads",
|
|
126
164
|
needs_identity=False,
|
|
127
165
|
takes_rest=False,
|
|
166
|
+
hidden=True,
|
|
128
167
|
),
|
|
129
168
|
# ── bug ──
|
|
130
|
-
Command("bug", "lib.board_bug", "cmd_bug", "bug tracker", "bug {report|assign|fix|list|overdue}"),
|
|
169
|
+
Command("bug", "lib.board_bug", "cmd_bug", "bug tracker", "bug {report|assign|fix|list|overdue}", hidden=True),
|
|
131
170
|
# ── task ──
|
|
132
|
-
Command("task", "lib.board_task", "cmd_task", "task queue management", "task {add|done|list|next}"),
|
|
171
|
+
Command("task", "lib.board_task", "cmd_task", "task queue management", "task {add|done|list|next}", hidden=True),
|
|
172
|
+
# ── ownership ──
|
|
173
|
+
Command("own", "lib.board_own", "cmd_own", "ownership registry", "own {claim|list|disown|map}"),
|
|
174
|
+
Command("scan", "lib.board_own", "cmd_scan", "scan issues/CI for owners", "scan"),
|
|
175
|
+
# ── heartbeat ──
|
|
176
|
+
Command(
|
|
177
|
+
"pulse", "lib.board_pulse", "cmd_pulse", "heartbeat + unread count", "pulse", takes_rest=False, hidden=True
|
|
178
|
+
),
|
|
133
179
|
# ── voting ──
|
|
134
|
-
Command("propose", "lib.board_vote", "cmd_propose", "create a proposal", "propose <内容> [--type S]"),
|
|
135
|
-
Command(
|
|
136
|
-
|
|
180
|
+
Command("propose", "lib.board_vote", "cmd_propose", "create a proposal", "propose <内容> [--type S]", hidden=True),
|
|
181
|
+
Command(
|
|
182
|
+
"vote", "lib.board_vote", "cmd_vote", "vote on a proposal", "vote <N> <SUPPORT|OBJECT> <reason>", hidden=True
|
|
183
|
+
),
|
|
184
|
+
Command("tally", "lib.board_vote", "cmd_tally", "recount votes", "tally <N>", needs_identity=False, hidden=True),
|
|
137
185
|
# ── mailbox (encrypted) ──
|
|
138
|
-
Command(
|
|
139
|
-
|
|
140
|
-
|
|
186
|
+
Command(
|
|
187
|
+
"keygen",
|
|
188
|
+
"lib.board_mailbox",
|
|
189
|
+
"cmd_keygen",
|
|
190
|
+
"generate encryption keypair",
|
|
191
|
+
"keygen",
|
|
192
|
+
takes_rest=False,
|
|
193
|
+
hidden=True,
|
|
194
|
+
),
|
|
195
|
+
Command(
|
|
196
|
+
"seal", "lib.board_mailbox", "cmd_seal", "send encrypted message", "seal <recipient> <message>", hidden=True
|
|
197
|
+
),
|
|
198
|
+
Command(
|
|
199
|
+
"unseal", "lib.board_mailbox", "cmd_unseal", "read encrypted inbox", "unseal", takes_rest=False, hidden=True
|
|
200
|
+
),
|
|
141
201
|
Command(
|
|
142
202
|
"mailbox-log",
|
|
143
203
|
"lib.board_mailbox",
|
|
@@ -145,9 +205,22 @@ COMMANDS: list[Command] = [
|
|
|
145
205
|
"encrypted message history",
|
|
146
206
|
"mailbox-log",
|
|
147
207
|
takes_rest=False,
|
|
208
|
+
hidden=True,
|
|
148
209
|
),
|
|
210
|
+
Command(
|
|
211
|
+
"keygen-all",
|
|
212
|
+
"lib.board_mailbox",
|
|
213
|
+
"cmd_keygen_all",
|
|
214
|
+
"generate keys for all sessions",
|
|
215
|
+
"keygen-all",
|
|
216
|
+
needs_identity=False,
|
|
217
|
+
takes_rest=False,
|
|
218
|
+
hidden=True,
|
|
219
|
+
),
|
|
220
|
+
# ── daily report ──
|
|
221
|
+
Command("daily", "lib.board_daily", "cmd_daily", "generate daily report", "daily [补充说明]"),
|
|
149
222
|
# ── admin ──
|
|
150
|
-
Command("kudos", "lib.board_admin", "cmd_kudos", "give public recognition", "kudos <target> <reason>"),
|
|
223
|
+
Command("kudos", "lib.board_admin", "cmd_kudos", "give public recognition", "kudos <target> <reason>", hidden=True),
|
|
151
224
|
Command(
|
|
152
225
|
"kudos-list",
|
|
153
226
|
"lib.board_admin",
|
|
@@ -157,12 +230,13 @@ COMMANDS: list[Command] = [
|
|
|
157
230
|
needs_identity=False,
|
|
158
231
|
takes_rest=False,
|
|
159
232
|
aliases=["kudos-board"],
|
|
233
|
+
hidden=True,
|
|
160
234
|
),
|
|
161
|
-
Command("suspend", "lib.board_admin", "cmd_suspend", "suspend a session", "suspend <session>"),
|
|
162
|
-
Command("resume", "lib.board_admin", "cmd_resume", "resume a session", "resume <session>"),
|
|
235
|
+
Command("suspend", "lib.board_admin", "cmd_suspend", "suspend a session", "suspend <session>", hidden=True),
|
|
236
|
+
Command("resume", "lib.board_admin", "cmd_resume", "resume a session", "resume <session>", hidden=True),
|
|
163
237
|
# ── git lock ──
|
|
164
|
-
Command("git-lock", "lib.board_lock", "cmd_git_lock", "acquire git index lock", "git-lock [reason]"),
|
|
165
|
-
Command("git-unlock", "lib.board_lock", "cmd_git_unlock", "release git index lock", "git-unlock"),
|
|
238
|
+
Command("git-lock", "lib.board_lock", "cmd_git_lock", "acquire git index lock", "git-lock [reason]", hidden=True),
|
|
239
|
+
Command("git-unlock", "lib.board_lock", "cmd_git_unlock", "release git index lock", "git-unlock", hidden=True),
|
|
166
240
|
Command(
|
|
167
241
|
"git-lock-status",
|
|
168
242
|
"lib.board_lock",
|
|
@@ -171,6 +245,7 @@ COMMANDS: list[Command] = [
|
|
|
171
245
|
"git-lock-status",
|
|
172
246
|
needs_identity=False,
|
|
173
247
|
takes_rest=False,
|
|
248
|
+
hidden=True,
|
|
174
249
|
),
|
|
175
250
|
# ── tui ──
|
|
176
251
|
Command(
|
|
@@ -182,6 +257,16 @@ COMMANDS: list[Command] = [
|
|
|
182
257
|
needs_identity=False,
|
|
183
258
|
takes_rest=False,
|
|
184
259
|
),
|
|
260
|
+
# ── mail ──
|
|
261
|
+
Command("mail", "lib.board_mail", "cmd_mail", "persistent mail with CC/threading", "mail {send|list|read|reply}"),
|
|
262
|
+
# ── pending actions ──
|
|
263
|
+
Command(
|
|
264
|
+
"pending",
|
|
265
|
+
"lib.board_pending",
|
|
266
|
+
"cmd_pending",
|
|
267
|
+
"pending actions queue",
|
|
268
|
+
"pending {add|list|verify|retry|resolve}",
|
|
269
|
+
),
|
|
185
270
|
# ── maintenance ──
|
|
186
271
|
Command(
|
|
187
272
|
"prune",
|
|
@@ -190,6 +275,7 @@ COMMANDS: list[Command] = [
|
|
|
190
275
|
"prune old messages",
|
|
191
276
|
"prune [--before DAYS] [--dry-run]",
|
|
192
277
|
needs_identity=False,
|
|
278
|
+
hidden=True,
|
|
193
279
|
),
|
|
194
280
|
Command(
|
|
195
281
|
"backup",
|
|
@@ -198,6 +284,7 @@ COMMANDS: list[Command] = [
|
|
|
198
284
|
"backup database",
|
|
199
285
|
"backup [--output <path>]",
|
|
200
286
|
needs_identity=False,
|
|
287
|
+
hidden=True,
|
|
201
288
|
),
|
|
202
289
|
Command(
|
|
203
290
|
"restore",
|
|
@@ -206,6 +293,7 @@ COMMANDS: list[Command] = [
|
|
|
206
293
|
"restore from backup",
|
|
207
294
|
"restore <file> [--force]",
|
|
208
295
|
needs_identity=False,
|
|
296
|
+
hidden=True,
|
|
209
297
|
),
|
|
210
298
|
]
|
|
211
299
|
|
|
@@ -246,18 +334,12 @@ def _fmt_command(cmd: Command, width: int) -> str:
|
|
|
246
334
|
|
|
247
335
|
|
|
248
336
|
def print_help() -> None:
|
|
249
|
-
|
|
250
|
-
|
|
337
|
+
visible = [c for c in COMMANDS if not c.hidden]
|
|
338
|
+
max_name = max(len(c.name) + (2 + len(", ".join(c.aliases)) if c.aliases else 0) for c in visible) + 2
|
|
339
|
+
print("board — 同学协作工具\n")
|
|
251
340
|
print("Usage: board --as <name> <command> [args...]\n")
|
|
252
341
|
print("Commands:")
|
|
253
|
-
|
|
254
|
-
for c in COMMANDS:
|
|
255
|
-
mod = c.module.rsplit(".", 1)[-1] # board_msg, board_view, etc.
|
|
256
|
-
if mod != last_module:
|
|
257
|
-
if last_module:
|
|
258
|
-
print()
|
|
259
|
-
print(f" [{mod}]")
|
|
260
|
-
last_module = mod
|
|
342
|
+
for c in visible:
|
|
261
343
|
print(_fmt_command(c, max_name))
|
|
262
344
|
print()
|
|
263
345
|
|
|
@@ -290,6 +372,9 @@ def main() -> None:
|
|
|
290
372
|
print("ERROR: identity required. Use: board --as <name> <command>", file=sys.stderr)
|
|
291
373
|
raise SystemExit(1)
|
|
292
374
|
|
|
375
|
+
if identity:
|
|
376
|
+
validate_identity(db, identity)
|
|
377
|
+
|
|
293
378
|
_dispatch(cmd, db, identity, rest)
|
|
294
379
|
|
|
295
380
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""check-changelog — enforce changelog discipline on release versions.
|
|
3
|
+
|
|
4
|
+
Rules:
|
|
5
|
+
1. Release versions (no -dev suffix) MUST have a matching ## {version} entry
|
|
6
|
+
in CHANGELOG.md with actual content (not just a header).
|
|
7
|
+
2. The entry must NOT be marked "(unreleased)".
|
|
8
|
+
3. Dev versions always pass — the unreleased section accumulates freely.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
./bin/check-changelog # check and exit 0/1
|
|
12
|
+
./bin/check-changelog --help # this message
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
20
|
+
VERSION_FILE = ROOT / "VERSION"
|
|
21
|
+
CHANGELOG_FILE = ROOT / "CHANGELOG.md"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_version() -> str:
|
|
25
|
+
return VERSION_FILE.read_text().strip()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_release(ver: str) -> bool:
|
|
29
|
+
return not ver.endswith("-dev")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_changelog_section(ver: str, text: str) -> tuple[bool, str]:
|
|
33
|
+
"""Find ## {ver} section in changelog. Returns (found, section_body)."""
|
|
34
|
+
pattern = re.compile(
|
|
35
|
+
rf"^## {re.escape(ver)}\b[^\n]*\n(.*?)(?=^## |\Z)",
|
|
36
|
+
re.MULTILINE | re.DOTALL,
|
|
37
|
+
)
|
|
38
|
+
m = pattern.search(text)
|
|
39
|
+
if not m:
|
|
40
|
+
return False, ""
|
|
41
|
+
return True, m.group(1).strip()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_not_unreleased(ver: str, text: str) -> bool:
|
|
45
|
+
"""Return True if the version header is NOT marked unreleased."""
|
|
46
|
+
pattern = re.compile(rf"^## {re.escape(ver)}.*\(unreleased\)", re.MULTILINE | re.IGNORECASE)
|
|
47
|
+
return not pattern.search(text)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def previous_release(text: str, current: str) -> str | None:
|
|
51
|
+
"""Find the most recent release version before current in CHANGELOG.md."""
|
|
52
|
+
versions = re.findall(r"^## (\d+\.\d+\.\d+)\b", text, re.MULTILINE)
|
|
53
|
+
for v in versions:
|
|
54
|
+
if v != current:
|
|
55
|
+
return v
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> None:
|
|
60
|
+
if "--help" in sys.argv or "-h" in sys.argv:
|
|
61
|
+
print(__doc__)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
ver = read_version()
|
|
65
|
+
|
|
66
|
+
if not is_release(ver):
|
|
67
|
+
print(f"OK {ver} is a dev version — changelog check skipped")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
if not CHANGELOG_FILE.exists():
|
|
71
|
+
print(f"ERROR: CHANGELOG.md not found. Release {ver} requires a changelog entry.")
|
|
72
|
+
raise SystemExit(1)
|
|
73
|
+
|
|
74
|
+
text = CHANGELOG_FILE.read_text()
|
|
75
|
+
|
|
76
|
+
found, body = find_changelog_section(ver, text)
|
|
77
|
+
if not found:
|
|
78
|
+
prev = previous_release(text, ver)
|
|
79
|
+
hint = f" Summarize all changes since {prev}." if prev else ""
|
|
80
|
+
print(f"ERROR: CHANGELOG.md has no entry for release {ver}.{hint}")
|
|
81
|
+
print(f"Add a '## {ver}' section with Features, Bug Fixes, etc.")
|
|
82
|
+
raise SystemExit(1)
|
|
83
|
+
|
|
84
|
+
if not check_not_unreleased(ver, text):
|
|
85
|
+
print(f"ERROR: CHANGELOG.md entry for {ver} is marked (unreleased). Remove the marker for a release.")
|
|
86
|
+
raise SystemExit(1)
|
|
87
|
+
|
|
88
|
+
content_lines = [line for line in body.splitlines() if line.strip() and not line.startswith("#")]
|
|
89
|
+
if len(content_lines) < 3:
|
|
90
|
+
print(f"ERROR: CHANGELOG.md entry for {ver} is too thin ({len(content_lines)} content lines).")
|
|
91
|
+
print("A release changelog must summarize all changes since the previous release.")
|
|
92
|
+
raise SystemExit(1)
|
|
93
|
+
|
|
94
|
+
print(f"OK CHANGELOG.md has a valid entry for release {ver} ({len(content_lines)} content lines)")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
cat <<'EOF'
|
|
6
|
+
Usage: bin/check-npm-package [--install-smoke] [--tarball PATH]
|
|
7
|
+
|
|
8
|
+
Build or inspect the npm package tarball, verify required files are present,
|
|
9
|
+
reject obvious secret-bearing paths, and optionally install the packed tarball
|
|
10
|
+
globally in a temporary prefix to prove the cnb entrypoint is runnable.
|
|
11
|
+
EOF
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
INSTALL_SMOKE=false
|
|
15
|
+
TARBALL=""
|
|
16
|
+
|
|
17
|
+
while [ $# -gt 0 ]; do
|
|
18
|
+
case "$1" in
|
|
19
|
+
--install-smoke)
|
|
20
|
+
INSTALL_SMOKE=true
|
|
21
|
+
shift
|
|
22
|
+
;;
|
|
23
|
+
--tarball)
|
|
24
|
+
if [ $# -lt 2 ]; then
|
|
25
|
+
echo "ERROR: --tarball requires a path" >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
TARBALL="$2"
|
|
29
|
+
shift 2
|
|
30
|
+
;;
|
|
31
|
+
-h|--help)
|
|
32
|
+
usage
|
|
33
|
+
exit 0
|
|
34
|
+
;;
|
|
35
|
+
*)
|
|
36
|
+
echo "ERROR: unknown argument: $1" >&2
|
|
37
|
+
usage >&2
|
|
38
|
+
exit 2
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
WORK_DIR="$(mktemp -d)"
|
|
44
|
+
cleanup() {
|
|
45
|
+
rm -rf "$WORK_DIR"
|
|
46
|
+
}
|
|
47
|
+
trap cleanup EXIT
|
|
48
|
+
|
|
49
|
+
if [ -z "$TARBALL" ]; then
|
|
50
|
+
tarball_name="$(npm pack --pack-destination "$WORK_DIR")"
|
|
51
|
+
TARBALL="$WORK_DIR/$tarball_name"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [ ! -f "$TARBALL" ]; then
|
|
55
|
+
echo "ERROR: tarball not found: $TARBALL" >&2
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
tar -tzf "$TARBALL" > "$WORK_DIR/files.txt"
|
|
60
|
+
|
|
61
|
+
required_files=(
|
|
62
|
+
"package/bin/cnb.js"
|
|
63
|
+
"package/bin/cnb"
|
|
64
|
+
"package/lib/cli.py"
|
|
65
|
+
"package/VERSION"
|
|
66
|
+
"package/package.json"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
for file in "${required_files[@]}"; do
|
|
70
|
+
if ! grep -qx "$file" "$WORK_DIR/files.txt"; then
|
|
71
|
+
echo "ERROR: npm package is missing required file: $file" >&2
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
|
|
76
|
+
if grep -E '(^|/)(\.env($|[._-])|\.npmrc$|id_(rsa|dsa|ecdsa|ed25519)$|[^/]+\.(pem|p12|pfx|key)$|credentials?($|[._-]))' "$WORK_DIR/files.txt"; then
|
|
77
|
+
echo "ERROR: npm package includes a path that looks secret-bearing" >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
if [ "$INSTALL_SMOKE" = true ]; then
|
|
82
|
+
prefix="$WORK_DIR/install"
|
|
83
|
+
project="$WORK_DIR/project"
|
|
84
|
+
mkdir -p "$prefix" "$project"
|
|
85
|
+
npm install --global --ignore-scripts --no-audit --no-fund --prefix "$prefix" "$TARBALL"
|
|
86
|
+
PATH="$prefix/bin:$PATH" CNB_PROJECT="$project" cnb --version | grep -E '^cnb v[0-9]+\.[0-9]+\.[0-9]+' >/dev/null
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
echo "OK npm package tarball passed checks: $TARBALL"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""check-readme-sync — verify README.md and README_zh.md have matching section structure.
|
|
3
|
+
|
|
4
|
+
Both files use <!-- section:NAME --> markers. This script checks that they
|
|
5
|
+
have the same markers in the same order. Content differs (that's the point
|
|
6
|
+
of two languages), but structure must match.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
bin/check-readme-sync # check and report
|
|
10
|
+
bin/check-readme-sync --fix # print which sections are missing/extra
|
|
11
|
+
|
|
12
|
+
Exit 0 if in sync, 1 if not.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
20
|
+
MARKER = re.compile(r"<!--\s*section:(\S+)\s*-->")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def extract_sections(path: Path) -> list[str]:
|
|
24
|
+
if not path.exists():
|
|
25
|
+
return []
|
|
26
|
+
return MARKER.findall(path.read_text())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
en = ROOT / "README.md"
|
|
31
|
+
zh = ROOT / "README_zh.md"
|
|
32
|
+
|
|
33
|
+
en_sections = extract_sections(en)
|
|
34
|
+
zh_sections = extract_sections(zh)
|
|
35
|
+
|
|
36
|
+
if not en_sections:
|
|
37
|
+
print(f"ERROR: no section markers in {en.name}")
|
|
38
|
+
raise SystemExit(1)
|
|
39
|
+
if not zh_sections:
|
|
40
|
+
print(f"ERROR: no section markers in {zh.name}")
|
|
41
|
+
raise SystemExit(1)
|
|
42
|
+
|
|
43
|
+
if en_sections == zh_sections:
|
|
44
|
+
print(f"OK {len(en_sections)} sections in sync: {', '.join(en_sections)}")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
en_set = set(en_sections)
|
|
48
|
+
zh_set = set(zh_sections)
|
|
49
|
+
|
|
50
|
+
missing_in_zh = en_set - zh_set
|
|
51
|
+
missing_in_en = zh_set - en_set
|
|
52
|
+
|
|
53
|
+
if missing_in_zh:
|
|
54
|
+
print(f"README_zh.md missing: {', '.join(sorted(missing_in_zh))}")
|
|
55
|
+
if missing_in_en:
|
|
56
|
+
print(f"README.md missing: {', '.join(sorted(missing_in_en))}")
|
|
57
|
+
|
|
58
|
+
if en_sections != zh_sections and not (missing_in_zh or missing_in_en):
|
|
59
|
+
print("Section order differs:")
|
|
60
|
+
for i, (e, z) in enumerate(zip(en_sections, zh_sections)):
|
|
61
|
+
if e != z:
|
|
62
|
+
print(f" position {i}: README.md has '{e}', README_zh.md has '{z}'")
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
raise SystemExit(1)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|