vibego 0.2.52__py3-none-any.whl → 1.0.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.
Potentially problematic release.
This version of vibego might be problematic. Click here for more details.
- bot.py +1557 -1431
- logging_setup.py +25 -18
- master.py +799 -508
- project_repository.py +42 -40
- scripts/__init__.py +1 -2
- scripts/bump_version.sh +57 -55
- scripts/log_writer.py +19 -16
- scripts/master_healthcheck.py +44 -44
- scripts/models/claudecode.sh +4 -4
- scripts/models/codex.sh +1 -1
- scripts/models/common.sh +24 -6
- scripts/models/gemini.sh +2 -2
- scripts/publish.sh +50 -50
- scripts/run_bot.sh +38 -17
- scripts/start.sh +136 -116
- scripts/start_tmux_codex.sh +8 -8
- scripts/stop_all.sh +21 -21
- scripts/stop_bot.sh +31 -10
- scripts/test_deps_check.sh +32 -28
- tasks/__init__.py +1 -1
- tasks/commands.py +4 -4
- tasks/constants.py +1 -1
- tasks/fsm.py +9 -9
- tasks/models.py +7 -7
- tasks/service.py +56 -56
- vibego-1.0.0.dist-info/METADATA +236 -0
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/RECORD +36 -35
- vibego-1.0.0.dist-info/licenses/LICENSE +201 -0
- vibego_cli/__init__.py +5 -4
- vibego_cli/__main__.py +1 -2
- vibego_cli/config.py +9 -9
- vibego_cli/deps.py +8 -9
- vibego_cli/main.py +63 -63
- vibego-0.2.52.dist-info/METADATA +0 -197
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/WHEEL +0 -0
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/entry_points.txt +0 -0
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/top_level.txt +0 -0
project_repository.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
1.
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
"""Project configuration repository syncing SQLite and JSON storage.
|
|
2
|
+
|
|
3
|
+
Responsibilities:
|
|
4
|
+
1. Initialise the SQLite database and import data from JSON on first run while
|
|
5
|
+
keeping a backup of the original JSON file.
|
|
6
|
+
2. Provide CRUD helpers that always write the latest JSON for legacy
|
|
7
|
+
compatibility.
|
|
8
|
+
3. Expose data structures consumed by the master process and related scripts.
|
|
7
9
|
"""
|
|
8
10
|
from __future__ import annotations
|
|
9
11
|
|
|
@@ -23,7 +25,7 @@ logger = logging.getLogger(__name__)
|
|
|
23
25
|
|
|
24
26
|
@dataclass(frozen=True)
|
|
25
27
|
class ProjectRecord:
|
|
26
|
-
"""
|
|
28
|
+
"""Represent the configuration data for a single project."""
|
|
27
29
|
|
|
28
30
|
bot_name: str
|
|
29
31
|
bot_token: str
|
|
@@ -34,7 +36,7 @@ class ProjectRecord:
|
|
|
34
36
|
legacy_name: Optional[str]
|
|
35
37
|
|
|
36
38
|
def to_dict(self) -> dict:
|
|
37
|
-
"""
|
|
39
|
+
"""Return a dictionary representation ready for JSON serialisation."""
|
|
38
40
|
return {
|
|
39
41
|
"bot_name": self.bot_name,
|
|
40
42
|
"bot_token": self.bot_token,
|
|
@@ -47,21 +49,21 @@ class ProjectRecord:
|
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
class ProjectRepository:
|
|
50
|
-
"""
|
|
52
|
+
"""Repository wrapper that encapsulates project configuration I/O."""
|
|
51
53
|
|
|
52
54
|
def __init__(self, db_path: Path, json_path: Path):
|
|
53
|
-
"""
|
|
55
|
+
"""Initialise the repository and ensure required files exist."""
|
|
54
56
|
|
|
55
|
-
#
|
|
57
|
+
# Store paths and create parent directories.
|
|
56
58
|
self.db_path = db_path
|
|
57
59
|
self.json_path = json_path
|
|
58
60
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
61
|
self.json_path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
-
#
|
|
62
|
+
# Prepare the database file.
|
|
61
63
|
self._initialize()
|
|
62
64
|
|
|
63
65
|
def _initialize(self) -> None:
|
|
64
|
-
"""
|
|
66
|
+
"""Initialise the database and import JSON data on first launch."""
|
|
65
67
|
first_create = not self.db_path.exists()
|
|
66
68
|
with self._connect() as conn:
|
|
67
69
|
conn.execute("PRAGMA foreign_keys = ON;")
|
|
@@ -83,27 +85,27 @@ class ProjectRepository:
|
|
|
83
85
|
)
|
|
84
86
|
if first_create:
|
|
85
87
|
self._import_from_json()
|
|
86
|
-
#
|
|
88
|
+
# Repair legacy records on every startup to keep data normalised.
|
|
87
89
|
self._repair_records()
|
|
88
|
-
#
|
|
90
|
+
# Export once on startup so JSON mirrors the database.
|
|
89
91
|
self._export_to_json(self.list_projects())
|
|
90
92
|
|
|
91
93
|
def _connect(self) -> sqlite3.Connection:
|
|
92
|
-
"""
|
|
94
|
+
"""Create a database connection with row dictionaries enabled."""
|
|
93
95
|
conn = sqlite3.connect(str(self.db_path))
|
|
94
96
|
conn.row_factory = sqlite3.Row
|
|
95
97
|
return conn
|
|
96
98
|
|
|
97
99
|
def _import_from_json(self) -> None:
|
|
98
|
-
"""
|
|
100
|
+
"""Import data from JSON during the first initialisation and keep a backup."""
|
|
99
101
|
if not self.json_path.exists():
|
|
100
102
|
return
|
|
101
103
|
try:
|
|
102
104
|
raw = json.loads(self.json_path.read_text(encoding="utf-8"))
|
|
103
105
|
except json.JSONDecodeError as exc:
|
|
104
|
-
raise RuntimeError(f"
|
|
106
|
+
raise RuntimeError(f"Failed to parse {self.json_path}: {exc}") from exc
|
|
105
107
|
if not isinstance(raw, list):
|
|
106
|
-
raise RuntimeError(f"{self.json_path}
|
|
108
|
+
raise RuntimeError(f"The content of {self.json_path} must be an array.")
|
|
107
109
|
records: List[ProjectRecord] = []
|
|
108
110
|
for item in raw:
|
|
109
111
|
if not isinstance(item, dict):
|
|
@@ -125,12 +127,12 @@ class ProjectRepository:
|
|
|
125
127
|
shutil.copy2(self.json_path, backup_path)
|
|
126
128
|
|
|
127
129
|
def _build_backup_path(self) -> Path:
|
|
128
|
-
"""
|
|
130
|
+
"""Build a timestamped JSON backup path to avoid overwriting files."""
|
|
129
131
|
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
|
130
132
|
return self.json_path.with_suffix(self.json_path.suffix + f".{timestamp}.bak")
|
|
131
133
|
|
|
132
134
|
def list_projects(self) -> List[ProjectRecord]:
|
|
133
|
-
"""
|
|
135
|
+
"""Return all project configurations."""
|
|
134
136
|
with self._connect() as conn:
|
|
135
137
|
cursor = conn.execute(
|
|
136
138
|
"""
|
|
@@ -144,7 +146,7 @@ class ProjectRepository:
|
|
|
144
146
|
return [self._normalize_record_fields(self._row_to_record(row, normalize=False)) for row in rows]
|
|
145
147
|
|
|
146
148
|
def get_by_slug(self, slug: str) -> Optional[ProjectRecord]:
|
|
147
|
-
"""
|
|
149
|
+
"""Look up a project by slug (case-insensitive for legacy data)."""
|
|
148
150
|
slug = self._sanitize_slug(slug)
|
|
149
151
|
with self._connect() as conn:
|
|
150
152
|
cursor = conn.execute(
|
|
@@ -161,7 +163,7 @@ class ProjectRepository:
|
|
|
161
163
|
return self._normalize_record_fields(self._row_to_record(row, normalize=False))
|
|
162
164
|
|
|
163
165
|
def get_by_bot_name(self, bot_name: str) -> Optional[ProjectRecord]:
|
|
164
|
-
"""
|
|
166
|
+
"""Look up a project by bot name."""
|
|
165
167
|
bot_name = self._sanitize_bot_name(bot_name)
|
|
166
168
|
with self._connect() as conn:
|
|
167
169
|
cursor = conn.execute(
|
|
@@ -178,7 +180,7 @@ class ProjectRepository:
|
|
|
178
180
|
return self._normalize_record_fields(self._row_to_record(row, normalize=False))
|
|
179
181
|
|
|
180
182
|
def insert_project(self, record: ProjectRecord) -> None:
|
|
181
|
-
"""
|
|
183
|
+
"""Insert a new project record."""
|
|
182
184
|
normalized = self._normalize_record_fields(record)
|
|
183
185
|
with self._connect() as conn:
|
|
184
186
|
conn.execute("BEGIN IMMEDIATE;")
|
|
@@ -203,7 +205,7 @@ class ProjectRepository:
|
|
|
203
205
|
self._export_to_json(self.list_projects())
|
|
204
206
|
|
|
205
207
|
def update_project(self, slug: str, record: ProjectRecord) -> None:
|
|
206
|
-
"""
|
|
208
|
+
"""Update a project using its slug (case-insensitive) as the identifier."""
|
|
207
209
|
normalized_slug = self._sanitize_slug(slug)
|
|
208
210
|
normalized = self._normalize_record_fields(record)
|
|
209
211
|
with self._connect() as conn:
|
|
@@ -228,12 +230,12 @@ class ProjectRepository:
|
|
|
228
230
|
)
|
|
229
231
|
if cursor.rowcount == 0:
|
|
230
232
|
conn.rollback()
|
|
231
|
-
raise ValueError(f"
|
|
233
|
+
raise ValueError(f"Project {slug} not found")
|
|
232
234
|
conn.commit()
|
|
233
235
|
self._export_to_json(self.list_projects())
|
|
234
236
|
|
|
235
237
|
def delete_project(self, slug: str) -> None:
|
|
236
|
-
"""
|
|
238
|
+
"""Delete a project by slug (case-insensitive)."""
|
|
237
239
|
normalized_slug = self._sanitize_slug(slug)
|
|
238
240
|
with self._connect() as conn:
|
|
239
241
|
conn.execute("BEGIN IMMEDIATE;")
|
|
@@ -243,12 +245,12 @@ class ProjectRepository:
|
|
|
243
245
|
)
|
|
244
246
|
if cursor.rowcount == 0:
|
|
245
247
|
conn.rollback()
|
|
246
|
-
raise ValueError(f"
|
|
248
|
+
raise ValueError(f"Project {slug} not found")
|
|
247
249
|
conn.commit()
|
|
248
250
|
self._export_to_json(self.list_projects())
|
|
249
251
|
|
|
250
252
|
def _bulk_upsert(self, records: Iterable[ProjectRecord]) -> None:
|
|
251
|
-
"""
|
|
253
|
+
"""Insert or update multiple project records, used for bootstrap imports."""
|
|
252
254
|
with self._connect() as conn:
|
|
253
255
|
conn.execute("BEGIN IMMEDIATE;")
|
|
254
256
|
for record in records:
|
|
@@ -281,7 +283,7 @@ class ProjectRepository:
|
|
|
281
283
|
conn.commit()
|
|
282
284
|
|
|
283
285
|
def _normalize_int(self, value: Optional[object]) -> Optional[int]:
|
|
284
|
-
"""
|
|
286
|
+
"""Convert the input to an int or None, handling string representations."""
|
|
285
287
|
if value is None:
|
|
286
288
|
return None
|
|
287
289
|
if isinstance(value, int):
|
|
@@ -291,28 +293,28 @@ class ProjectRepository:
|
|
|
291
293
|
return None
|
|
292
294
|
|
|
293
295
|
def _sanitize_bot_name(self, bot_name: str) -> str:
|
|
294
|
-
"""
|
|
296
|
+
"""Trim whitespace and leading @ symbols to normalise bot names."""
|
|
295
297
|
cleaned = (bot_name or "").strip()
|
|
296
298
|
if cleaned.startswith("@"):
|
|
297
299
|
cleaned = cleaned[1:]
|
|
298
300
|
return cleaned.strip()
|
|
299
301
|
|
|
300
302
|
def _sanitize_slug(self, slug: str) -> str:
|
|
301
|
-
"""
|
|
303
|
+
"""Normalise slugs to lowercase and replace illegal characters."""
|
|
302
304
|
text = (slug or "").strip().lower()
|
|
303
305
|
text = text.replace(" ", "-").replace("/", "-").replace("\\", "-")
|
|
304
306
|
text = text.strip("-")
|
|
305
307
|
return text or "project"
|
|
306
308
|
|
|
307
309
|
def _sanitize_optional_text(self, value: Optional[str]) -> Optional[str]:
|
|
308
|
-
"""
|
|
310
|
+
"""Clean optional text values and return None for empty strings."""
|
|
309
311
|
if value is None:
|
|
310
312
|
return None
|
|
311
313
|
cleaned = value.strip()
|
|
312
314
|
return cleaned or None
|
|
313
315
|
|
|
314
316
|
def _normalize_record_fields(self, record: ProjectRecord) -> ProjectRecord:
|
|
315
|
-
"""
|
|
317
|
+
"""Return a normalised record to avoid persisting invalid values."""
|
|
316
318
|
allowed_chat_id = self._normalize_int(record.allowed_chat_id)
|
|
317
319
|
clean_bot = self._sanitize_bot_name(record.bot_name)
|
|
318
320
|
slug_source = record.project_slug.strip() or clean_bot
|
|
@@ -331,7 +333,7 @@ class ProjectRepository:
|
|
|
331
333
|
)
|
|
332
334
|
|
|
333
335
|
def _row_to_record(self, row: sqlite3.Row, *, normalize: bool = True) -> ProjectRecord:
|
|
334
|
-
"""
|
|
336
|
+
"""Convert a database row into ProjectRecord, optionally normalising it."""
|
|
335
337
|
record = ProjectRecord(
|
|
336
338
|
bot_name=row["bot_name"],
|
|
337
339
|
bot_token=row["bot_token"],
|
|
@@ -344,7 +346,7 @@ class ProjectRepository:
|
|
|
344
346
|
return self._normalize_record_fields(record) if normalize else record
|
|
345
347
|
|
|
346
348
|
def _repair_records(self) -> None:
|
|
347
|
-
"""
|
|
349
|
+
"""Repair legacy data on startup to ensure slugs and bot names are valid."""
|
|
348
350
|
with self._connect() as conn:
|
|
349
351
|
conn.row_factory = sqlite3.Row
|
|
350
352
|
cursor = conn.execute(
|
|
@@ -367,13 +369,13 @@ class ProjectRepository:
|
|
|
367
369
|
existing_slug_id = slug_owner.get(normalized.project_slug)
|
|
368
370
|
if existing_slug_id is not None and existing_slug_id != current_id:
|
|
369
371
|
raise RuntimeError(
|
|
370
|
-
f"
|
|
372
|
+
f"Slug normalisation conflict: {normalized.project_slug} is already used by record {existing_slug_id}"
|
|
371
373
|
)
|
|
372
374
|
slug_owner[normalized.project_slug] = current_id
|
|
373
375
|
existing_bot_id = bot_owner.get(normalized.bot_name)
|
|
374
376
|
if existing_bot_id is not None and existing_bot_id != current_id:
|
|
375
377
|
raise RuntimeError(
|
|
376
|
-
f"
|
|
378
|
+
f"Bot name normalisation conflict: {normalized.bot_name} is already used by record {existing_bot_id}"
|
|
377
379
|
)
|
|
378
380
|
bot_owner[normalized.bot_name] = current_id
|
|
379
381
|
if normalized != record:
|
|
@@ -402,10 +404,10 @@ class ProjectRepository:
|
|
|
402
404
|
),
|
|
403
405
|
)
|
|
404
406
|
conn.commit()
|
|
405
|
-
logger.info("
|
|
407
|
+
logger.info("Repaired %s project records and normalised slug/bot formats", len(updates))
|
|
406
408
|
|
|
407
409
|
def _export_to_json(self, records: Iterable[ProjectRecord]) -> None:
|
|
408
|
-
"""
|
|
410
|
+
"""Export the database content to JSON, keeping it readable for legacy flows."""
|
|
409
411
|
payload = [record.to_dict() for record in records]
|
|
410
412
|
tmp_path = self.json_path.with_suffix(self.json_path.suffix + ".tmp")
|
|
411
413
|
tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
scripts/__init__.py
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
1
|
+
"""Marker module so packaging tools treat this directory as a package."""
|
scripts/bump_version.sh
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
2
|
+
# Convenient script for version management
|
|
3
|
+
# How to use:
|
|
4
4
|
# ./scripts/bump_version.sh patch
|
|
5
5
|
# ./scripts/bump_version.sh minor
|
|
6
6
|
# ./scripts/bump_version.sh major
|
|
@@ -9,81 +9,83 @@
|
|
|
9
9
|
|
|
10
10
|
set -e
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# Project root directory
|
|
13
13
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
14
14
|
cd "$PROJECT_ROOT"
|
|
15
15
|
|
|
16
|
-
# bump-my-version
|
|
17
|
-
|
|
16
|
+
# bump-my-version path
|
|
17
|
+
MASTER_CONFIG_ROOT="${MASTER_CONFIG_ROOT:-$HOME/.config/vibego}"
|
|
18
|
+
RUNTIME_DIR="${VIBEGO_RUNTIME_ROOT:-$MASTER_CONFIG_ROOT/runtime}"
|
|
19
|
+
BUMP_CMD="$RUNTIME_DIR/.venv/bin/bump-my-version"
|
|
18
20
|
|
|
19
|
-
#
|
|
21
|
+
# Check if bump-my-version exists
|
|
20
22
|
if [ ! -f "$BUMP_CMD" ]; then
|
|
21
|
-
echo "
|
|
22
|
-
echo "
|
|
23
|
+
echo "Error: bump-my-version not found"
|
|
24
|
+
echo "Please install first: pip install bump-my-version"
|
|
23
25
|
exit 1
|
|
24
26
|
fi
|
|
25
27
|
|
|
26
|
-
#
|
|
28
|
+
# If there are no parameters, display help
|
|
27
29
|
if [ $# -eq 0 ]; then
|
|
28
|
-
echo "
|
|
29
|
-
echo " $0 patch
|
|
30
|
-
echo "
|
|
31
|
-
echo " $0 minor
|
|
32
|
-
echo "
|
|
33
|
-
echo " $0 major
|
|
34
|
-
echo "
|
|
35
|
-
echo " $0 show
|
|
36
|
-
echo " $0 --dry-run
|
|
30
|
+
echo "usage:"
|
|
31
|
+
echo " $0 patch Increment patch version (0.2.11 → 0.2.12)"
|
|
32
|
+
echo " Autocommit: fix: bugfixes"
|
|
33
|
+
echo " $0 minor Increment minor version (0.2.11 → 0.3.0)"
|
|
34
|
+
echo " Automatic submission: feat: Add new features"
|
|
35
|
+
echo " $0 major Increment major version (0.2.11 → 1.0.0)"
|
|
36
|
+
echo " Automatic submission: feat!: Major changes"
|
|
37
|
+
echo " $0 show Show current version"
|
|
38
|
+
echo " $0 --dry-run Preview changes (added in patch/minor/major back)"
|
|
37
39
|
echo ""
|
|
38
|
-
echo "
|
|
39
|
-
echo "
|
|
40
|
-
echo "
|
|
40
|
+
echo "illustrate:"
|
|
41
|
+
echo " The script automatically commits the currently uncommitted changes and then increments the version number."
|
|
42
|
+
echo " If you don't want to commit automatically, add --no-auto-commit to the parameters"
|
|
41
43
|
echo ""
|
|
42
|
-
echo "
|
|
43
|
-
echo " $0 patch #
|
|
44
|
-
echo " $0 patch --dry-run #
|
|
45
|
-
echo " $0 minor --no-auto-commit #
|
|
44
|
+
echo "Example:"
|
|
45
|
+
echo " $0 patch # Automatically commit changes andIncrement patch version"
|
|
46
|
+
echo " $0 patch --dry-run # The preview patch version is incremented (will not be submitted)"
|
|
47
|
+
echo " $0 minor --no-auto-commit # Only increments version, does not automatically commit current modifications"
|
|
46
48
|
exit 0
|
|
47
49
|
fi
|
|
48
50
|
|
|
49
|
-
#
|
|
51
|
+
# Handling show commands
|
|
50
52
|
if [ "$1" = "show" ]; then
|
|
51
53
|
"$BUMP_CMD" show current_version
|
|
52
54
|
exit 0
|
|
53
55
|
fi
|
|
54
56
|
|
|
55
|
-
#
|
|
57
|
+
# Processing --help
|
|
56
58
|
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
|
57
59
|
"$BUMP_CMD" --help
|
|
58
60
|
exit 0
|
|
59
61
|
fi
|
|
60
62
|
|
|
61
|
-
#
|
|
63
|
+
# Check if autocommit is disabled
|
|
62
64
|
AUTO_COMMIT=true
|
|
63
65
|
if [[ "$*" =~ "--no-auto-commit" ]]; then
|
|
64
66
|
AUTO_COMMIT=false
|
|
65
67
|
fi
|
|
66
68
|
|
|
67
|
-
#
|
|
69
|
+
# Check if it is dry-run
|
|
68
70
|
DRY_RUN=false
|
|
69
71
|
if [[ "$*" =~ "--dry-run" ]]; then
|
|
70
72
|
DRY_RUN=true
|
|
71
73
|
fi
|
|
72
74
|
|
|
73
|
-
#
|
|
75
|
+
# Get version type
|
|
74
76
|
VERSION_TYPE="$1"
|
|
75
77
|
|
|
76
|
-
#
|
|
78
|
+
# Get the commit message of the corresponding version type
|
|
77
79
|
get_commit_message() {
|
|
78
80
|
case "$1" in
|
|
79
81
|
patch)
|
|
80
82
|
echo "fix: bugfixes"
|
|
81
83
|
;;
|
|
82
84
|
minor)
|
|
83
|
-
echo "feat:
|
|
85
|
+
echo "feat: Add new features"
|
|
84
86
|
;;
|
|
85
87
|
major)
|
|
86
|
-
echo "feat!:
|
|
88
|
+
echo "feat!: Major changes"
|
|
87
89
|
;;
|
|
88
90
|
*)
|
|
89
91
|
echo ""
|
|
@@ -91,62 +93,62 @@ get_commit_message() {
|
|
|
91
93
|
esac
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
#
|
|
96
|
+
# Check if the version type is valid
|
|
95
97
|
COMMIT_MSG=$(get_commit_message "$VERSION_TYPE")
|
|
96
98
|
if [ -z "$COMMIT_MSG" ]; then
|
|
97
|
-
#
|
|
99
|
+
# If not a valid version type, pass it directly to bump-my-version
|
|
98
100
|
"$BUMP_CMD" bump "$@"
|
|
99
101
|
exit 0
|
|
100
102
|
fi
|
|
101
103
|
|
|
102
|
-
#
|
|
103
|
-
echo "📦
|
|
104
|
+
# Show current version
|
|
105
|
+
echo "📦 Current version:$("$BUMP_CMD" show current_version)"
|
|
104
106
|
echo ""
|
|
105
107
|
|
|
106
|
-
#
|
|
108
|
+
# Check for uncommitted changes
|
|
107
109
|
if [ "$AUTO_COMMIT" = true ] && [ "$DRY_RUN" = false ]; then
|
|
108
110
|
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
|
|
109
|
-
echo "📝
|
|
111
|
+
echo "📝 Uncommitted changes detected, prepare to create commit..."
|
|
110
112
|
echo ""
|
|
111
113
|
|
|
112
|
-
echo "Commit
|
|
114
|
+
echo "Commit information:$COMMIT_MSG"
|
|
113
115
|
echo ""
|
|
114
116
|
|
|
115
|
-
#
|
|
116
|
-
echo "
|
|
117
|
+
# Show files to be submitted
|
|
118
|
+
echo "Documents to be submitted:"
|
|
117
119
|
git status --short
|
|
118
120
|
echo ""
|
|
119
121
|
|
|
120
|
-
#
|
|
122
|
+
# Commit all changes
|
|
121
123
|
git add .
|
|
122
124
|
git commit -m "$COMMIT_MSG"
|
|
123
125
|
|
|
124
|
-
echo "
|
|
126
|
+
echo "OK: Code modification has been submitted"
|
|
125
127
|
echo ""
|
|
126
128
|
else
|
|
127
|
-
echo "ℹ️
|
|
129
|
+
echo "ℹ️ No uncommitted changes, skip automatic commit"
|
|
128
130
|
echo ""
|
|
129
131
|
fi
|
|
130
132
|
fi
|
|
131
133
|
|
|
132
|
-
#
|
|
133
|
-
echo "🚀
|
|
134
|
+
# Execution version increment
|
|
135
|
+
echo "🚀 Start incremental version..."
|
|
134
136
|
echo ""
|
|
135
137
|
|
|
136
138
|
"$BUMP_CMD" bump "$@"
|
|
137
139
|
|
|
138
140
|
echo ""
|
|
139
|
-
echo "
|
|
141
|
+
echo "OK: Version management completed!"
|
|
140
142
|
echo ""
|
|
141
|
-
echo "📋
|
|
143
|
+
echo "📋 Operation summary:"
|
|
142
144
|
if [ "$AUTO_COMMIT" = true ] && [ "$DRY_RUN" = false ]; then
|
|
143
|
-
echo " 1.
|
|
144
|
-
echo " 2.
|
|
145
|
-
echo " 3.
|
|
145
|
+
echo " 1. Submitted code modifications (if any)"
|
|
146
|
+
echo " 2. Version number incremented"
|
|
147
|
+
echo " 3. Version commit and tag created"
|
|
146
148
|
else
|
|
147
|
-
echo " 1.
|
|
148
|
-
echo " 2.
|
|
149
|
+
echo " 1. Version number incremented"
|
|
150
|
+
echo " 2. Version commit and tag created"
|
|
149
151
|
fi
|
|
150
152
|
echo ""
|
|
151
|
-
echo "💡
|
|
153
|
+
echo "💡 Tip: To push to remote, please execute:"
|
|
152
154
|
echo " git push && git push --tags"
|
scripts/log_writer.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
"""Log writer for tmux pipe-pane.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Stream stdin to a target log file while keeping the primary file under a
|
|
6
|
+
configurable size (default 20 MB).
|
|
7
|
+
- When the threshold is exceeded, archive the current file with a timestamp
|
|
8
|
+
suffix and create a fresh primary log.
|
|
9
|
+
- Periodically delete archived logs that exceed the retention window
|
|
10
|
+
(default 24 hours).
|
|
11
|
+
|
|
12
|
+
Environment variables:
|
|
13
|
+
- MODEL_LOG_MAX_BYTES: maximum size of the primary log file, default 20971520 (20 MB).
|
|
14
|
+
- MODEL_LOG_RETENTION_SECONDS: retention period for archived files, default 86400 seconds (24 h).
|
|
12
15
|
"""
|
|
13
16
|
|
|
14
17
|
from __future__ import annotations
|
|
@@ -25,7 +28,7 @@ DEFAULT_RETENTION_SECONDS = 24 * 60 * 60
|
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
def _env_int(name: str, default: int) -> int:
|
|
28
|
-
"""
|
|
31
|
+
"""Read an integer environment variable with graceful fallback."""
|
|
29
32
|
|
|
30
33
|
raw = os.environ.get(name)
|
|
31
34
|
if raw is None:
|
|
@@ -41,7 +44,7 @@ def _env_int(name: str, default: int) -> int:
|
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
def cleanup_archives(base_path: Path, retention_seconds: int) -> None:
|
|
44
|
-
"""
|
|
47
|
+
"""Remove archived logs that exceed the retention period."""
|
|
45
48
|
|
|
46
49
|
cutoff = time.time() - retention_seconds
|
|
47
50
|
pattern = f"{base_path.stem}-*.log"
|
|
@@ -58,7 +61,7 @@ def cleanup_archives(base_path: Path, retention_seconds: int) -> None:
|
|
|
58
61
|
|
|
59
62
|
|
|
60
63
|
def rotate_log(base_path: Path) -> Path:
|
|
61
|
-
"""
|
|
64
|
+
"""Archive the current primary log and return the archive path."""
|
|
62
65
|
|
|
63
66
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
64
67
|
archive_name = f"{base_path.stem}-{timestamp}.log"
|
|
@@ -72,13 +75,13 @@ def rotate_log(base_path: Path) -> Path:
|
|
|
72
75
|
try:
|
|
73
76
|
base_path.rename(archive_path)
|
|
74
77
|
except FileNotFoundError:
|
|
75
|
-
#
|
|
78
|
+
# External processes may have removed the file; skip archival in that case.
|
|
76
79
|
return archive_path
|
|
77
80
|
return archive_path
|
|
78
81
|
|
|
79
82
|
|
|
80
83
|
def main() -> int:
|
|
81
|
-
"""
|
|
84
|
+
"""Run the logging loop, handling rotation and archive cleanup."""
|
|
82
85
|
|
|
83
86
|
if len(sys.argv) != 2:
|
|
84
87
|
sys.stderr.write("Usage: log_writer.py <log_file>\n")
|
|
@@ -91,7 +94,7 @@ def main() -> int:
|
|
|
91
94
|
retention_seconds = _env_int("MODEL_LOG_RETENTION_SECONDS", DEFAULT_RETENTION_SECONDS)
|
|
92
95
|
|
|
93
96
|
def open_log_file() -> tuple[int, object]:
|
|
94
|
-
"""
|
|
97
|
+
"""Open the log file and return the current size together with the handle."""
|
|
95
98
|
|
|
96
99
|
fp = log_path.open("ab", buffering=0)
|
|
97
100
|
try:
|