@mindfoldhq/trellis 0.5.10 → 0.5.12
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/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +8 -108
- package/dist/commands/update.js.map +1 -1
- package/dist/migrations/manifests/0.5.11.json +16 -0
- package/dist/migrations/manifests/0.5.12.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.5.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.6.json +16 -0
- package/dist/templates/common/commands/start.md +2 -0
- package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +68 -0
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +289 -43
- package/dist/templates/trellis/config.yaml +18 -0
- package/dist/templates/trellis/scripts/add_session.py +15 -9
- package/dist/templates/trellis/scripts/common/config.py +57 -1
- package/dist/templates/trellis/scripts/common/safe_commit.py +52 -26
- package/dist/templates/trellis/scripts/common/session_context.py +170 -0
- package/dist/templates/trellis/scripts/common/task_store.py +17 -10
- package/package.json +1 -1
|
@@ -15,14 +15,19 @@ Design
|
|
|
15
15
|
------
|
|
16
16
|
- Scripts only stage SPECIFIC product paths (journal files, index.md, the
|
|
17
17
|
current task dir, the archive dir). Never the whole `.trellis/` tree.
|
|
18
|
-
- If plain `git add <specific>` fails with "ignored by", retry with
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
- If plain `git add <specific>` fails with "ignored by", DO NOT retry with
|
|
19
|
+
``-f``. The presence of `.trellis/` in `.gitignore` is treated as user
|
|
20
|
+
intent ("keep .trellis/ local-only"). The script warns and skips the
|
|
21
|
+
auto-commit; users who want auto-staging can either fix their `.gitignore`
|
|
22
|
+
or set ``session_auto_commit: false`` and manage git themselves.
|
|
23
|
+
- The warning includes a negative example: ``Do NOT use `git add -f .trellis/` ...``
|
|
24
|
+
so any AI rereading the log doesn't reinvent the bug.
|
|
25
|
+
|
|
26
|
+
History note: 0.5.10 introduced an automatic ``git add -f`` retry on the
|
|
27
|
+
specific paths. That was reverted in 0.5.11 — auto-forcing into a tree the
|
|
28
|
+
user had gitignored violates user intent even when the path list is narrow.
|
|
29
|
+
The wider-grain forbidden command stays forbidden, and the narrow-grain auto
|
|
30
|
+
``-f`` is gone too.
|
|
26
31
|
"""
|
|
27
32
|
|
|
28
33
|
from __future__ import annotations
|
|
@@ -145,20 +150,18 @@ def _stderr_indicates_ignored(stderr: str) -> bool:
|
|
|
145
150
|
def safe_git_add(
|
|
146
151
|
paths: list[str], repo_root: Path
|
|
147
152
|
) -> tuple[bool, bool, str]:
|
|
148
|
-
"""Run `git add` on specific paths
|
|
153
|
+
"""Run `git add` on specific paths; never retry with -f.
|
|
149
154
|
|
|
150
|
-
Returns (success, used_force, stderr)
|
|
151
|
-
|
|
155
|
+
Returns ``(success, used_force, stderr)``. The ``used_force`` field is
|
|
156
|
+
kept for signature compatibility with the 0.5.10 implementation but is
|
|
157
|
+
always ``False`` — we never auto-force.
|
|
152
158
|
|
|
153
159
|
Behavior:
|
|
154
160
|
- No paths passed → success, no force, empty stderr.
|
|
155
|
-
- Plain
|
|
156
|
-
- Plain fails
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
warning (see :func:`print_gitignore_warning`).
|
|
160
|
-
- Plain fails with a non-ignored error → return failure; do NOT retry
|
|
161
|
-
with -f (we only force when ignore is the cause).
|
|
161
|
+
- Plain ``git add -- <paths>`` succeeds → return success.
|
|
162
|
+
- Plain fails (any reason — ignored or otherwise) → return failure with
|
|
163
|
+
the stderr. Callers should inspect the stderr (see
|
|
164
|
+
:func:`print_gitignore_warning`) and skip the auto-commit.
|
|
162
165
|
"""
|
|
163
166
|
if not paths:
|
|
164
167
|
return True, False, ""
|
|
@@ -166,14 +169,7 @@ def safe_git_add(
|
|
|
166
169
|
rc, _, err = run_git(["add", "--", *paths], cwd=repo_root)
|
|
167
170
|
if rc == 0:
|
|
168
171
|
return True, False, ""
|
|
169
|
-
|
|
170
|
-
if not _stderr_indicates_ignored(err):
|
|
171
|
-
return False, False, err
|
|
172
|
-
|
|
173
|
-
rc2, _, err2 = run_git(["add", "-f", "--", *paths], cwd=repo_root)
|
|
174
|
-
if rc2 == 0:
|
|
175
|
-
return True, True, err2 or err
|
|
176
|
-
return False, True, err2 or err
|
|
172
|
+
return False, False, err
|
|
177
173
|
|
|
178
174
|
|
|
179
175
|
def print_gitignore_warning(paths: list[str]) -> None:
|
|
@@ -187,6 +183,15 @@ def print_gitignore_warning(paths: list[str]) -> None:
|
|
|
187
183
|
"[WARN] git add failed because .trellis/ paths are ignored by your .gitignore.",
|
|
188
184
|
file=sys.stderr,
|
|
189
185
|
)
|
|
186
|
+
print(
|
|
187
|
+
"[WARN] Skipping auto-commit. The journal/task files were still written to disk;",
|
|
188
|
+
file=sys.stderr,
|
|
189
|
+
)
|
|
190
|
+
print(
|
|
191
|
+
"[WARN] git was not touched.",
|
|
192
|
+
file=sys.stderr,
|
|
193
|
+
)
|
|
194
|
+
print("[WARN]", file=sys.stderr)
|
|
190
195
|
print(
|
|
191
196
|
"[WARN] Trellis manages these specific paths and they should be tracked:",
|
|
192
197
|
file=sys.stderr,
|
|
@@ -219,6 +224,27 @@ def print_gitignore_warning(paths: list[str]) -> None:
|
|
|
219
224
|
for sub in TRELLIS_IGNORED_SUBPATHS:
|
|
220
225
|
print(f"[WARN] {sub}", file=sys.stderr)
|
|
221
226
|
print("[WARN]", file=sys.stderr)
|
|
227
|
+
print(
|
|
228
|
+
"[WARN] Or, if you intentionally keep .trellis/ local-only, set in",
|
|
229
|
+
file=sys.stderr,
|
|
230
|
+
)
|
|
231
|
+
print(
|
|
232
|
+
"[WARN] .trellis/config.yaml:",
|
|
233
|
+
file=sys.stderr,
|
|
234
|
+
)
|
|
235
|
+
print(
|
|
236
|
+
"[WARN] session_auto_commit: false",
|
|
237
|
+
file=sys.stderr,
|
|
238
|
+
)
|
|
239
|
+
print(
|
|
240
|
+
"[WARN] so the scripts skip git entirely and you can review / commit",
|
|
241
|
+
file=sys.stderr,
|
|
242
|
+
)
|
|
243
|
+
print(
|
|
244
|
+
"[WARN] manually with `git status` / `git add` / `git commit`.",
|
|
245
|
+
file=sys.stderr,
|
|
246
|
+
)
|
|
247
|
+
print("[WARN]", file=sys.stderr)
|
|
222
248
|
print(
|
|
223
249
|
"[WARN] Do NOT use `git add -f .trellis/` — it pulls in backups, worktrees,",
|
|
224
250
|
file=sys.stderr,
|
|
@@ -14,8 +14,12 @@ Provides:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import subprocess
|
|
17
20
|
from pathlib import Path
|
|
18
21
|
|
|
22
|
+
from .active_task import resolve_context_key
|
|
19
23
|
from .config import get_git_packages
|
|
20
24
|
from .git import run_git
|
|
21
25
|
from .packages_context import get_packages_section
|
|
@@ -40,6 +44,14 @@ from .paths import (
|
|
|
40
44
|
# Helpers
|
|
41
45
|
# =============================================================================
|
|
42
46
|
|
|
47
|
+
_PACKAGE_NAME = "@mindfoldhq/trellis"
|
|
48
|
+
_UPDATE_CHECK_TIMEOUT_SECONDS = 1.0
|
|
49
|
+
_VERSION_RE = re.compile(
|
|
50
|
+
r"^\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?\s*$"
|
|
51
|
+
)
|
|
52
|
+
_VERSION_TOKEN_RE = re.compile(r"\b\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?\b")
|
|
53
|
+
|
|
54
|
+
|
|
43
55
|
def _collect_package_git_info(repo_root: Path) -> list[dict]:
|
|
44
56
|
"""Collect git status and recent commits for packages with independent git repos.
|
|
45
57
|
|
|
@@ -109,6 +121,158 @@ def _append_package_git_context(lines: list[str], package_git_info: list[dict])
|
|
|
109
121
|
lines.append("")
|
|
110
122
|
|
|
111
123
|
|
|
124
|
+
def _read_project_version(repo_root: Path) -> str | None:
|
|
125
|
+
try:
|
|
126
|
+
version = (repo_root / DIR_WORKFLOW / ".version").read_text(
|
|
127
|
+
encoding="utf-8"
|
|
128
|
+
).strip()
|
|
129
|
+
except OSError:
|
|
130
|
+
return None
|
|
131
|
+
return version or None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _fetch_trellis_version_output() -> str | None:
|
|
135
|
+
try:
|
|
136
|
+
result = subprocess.run(
|
|
137
|
+
["trellis", "--version"],
|
|
138
|
+
capture_output=True,
|
|
139
|
+
text=True,
|
|
140
|
+
encoding="utf-8",
|
|
141
|
+
errors="replace",
|
|
142
|
+
timeout=_UPDATE_CHECK_TIMEOUT_SECONDS,
|
|
143
|
+
)
|
|
144
|
+
except (OSError, subprocess.SubprocessError, TimeoutError):
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
if result.returncode != 0:
|
|
148
|
+
return None
|
|
149
|
+
output = f"{result.stdout}\n{result.stderr}".strip()
|
|
150
|
+
return output or None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _extract_available_update_version(output: str) -> str | None:
|
|
154
|
+
update_match = re.search(
|
|
155
|
+
r"Trellis update available:\s*"
|
|
156
|
+
r"(?P<current>\S+)\s*(?:→|->)\s*(?P<latest>\S+)",
|
|
157
|
+
output,
|
|
158
|
+
)
|
|
159
|
+
if update_match:
|
|
160
|
+
return update_match.group("latest").strip()
|
|
161
|
+
candidates = _VERSION_TOKEN_RE.findall(output)
|
|
162
|
+
return candidates[-1] if candidates else None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _resolve_available_update_version() -> str | None:
|
|
166
|
+
output = _fetch_trellis_version_output()
|
|
167
|
+
if not output:
|
|
168
|
+
return None
|
|
169
|
+
return _extract_available_update_version(output)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _parse_version(version: str) -> tuple[tuple[int, int, int], tuple[str, ...] | None] | None:
|
|
173
|
+
match = _VERSION_RE.match(version)
|
|
174
|
+
if not match:
|
|
175
|
+
return None
|
|
176
|
+
major, minor, patch, prerelease = match.groups()
|
|
177
|
+
numbers = (int(major), int(minor or "0"), int(patch or "0"))
|
|
178
|
+
prerelease_parts = tuple(prerelease.split(".")) if prerelease else None
|
|
179
|
+
return numbers, prerelease_parts
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _compare_prerelease(
|
|
183
|
+
left: tuple[str, ...] | None,
|
|
184
|
+
right: tuple[str, ...] | None,
|
|
185
|
+
) -> int:
|
|
186
|
+
if left is None and right is None:
|
|
187
|
+
return 0
|
|
188
|
+
if left is None:
|
|
189
|
+
return 1
|
|
190
|
+
if right is None:
|
|
191
|
+
return -1
|
|
192
|
+
|
|
193
|
+
for left_part, right_part in zip(left, right):
|
|
194
|
+
if left_part == right_part:
|
|
195
|
+
continue
|
|
196
|
+
left_numeric = left_part.isdigit()
|
|
197
|
+
right_numeric = right_part.isdigit()
|
|
198
|
+
if left_numeric and right_numeric:
|
|
199
|
+
left_int = int(left_part)
|
|
200
|
+
right_int = int(right_part)
|
|
201
|
+
return (left_int > right_int) - (left_int < right_int)
|
|
202
|
+
if left_numeric:
|
|
203
|
+
return -1
|
|
204
|
+
if right_numeric:
|
|
205
|
+
return 1
|
|
206
|
+
return (left_part > right_part) - (left_part < right_part)
|
|
207
|
+
|
|
208
|
+
return (len(left) > len(right)) - (len(left) < len(right))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _compare_versions(left: str, right: str) -> int | None:
|
|
212
|
+
parsed_left = _parse_version(left)
|
|
213
|
+
parsed_right = _parse_version(right)
|
|
214
|
+
if parsed_left is None or parsed_right is None:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
left_numbers, left_prerelease = parsed_left
|
|
218
|
+
right_numbers, right_prerelease = parsed_right
|
|
219
|
+
if left_numbers != right_numbers:
|
|
220
|
+
return (left_numbers > right_numbers) - (left_numbers < right_numbers)
|
|
221
|
+
return _compare_prerelease(left_prerelease, right_prerelease)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _update_marker_path(repo_root: Path) -> Path:
|
|
225
|
+
context_key = resolve_context_key()
|
|
226
|
+
if not context_key:
|
|
227
|
+
terminal_key = os.environ.get("TERM_SESSION_ID", "").strip()
|
|
228
|
+
context_key = terminal_key or f"ppid-{os.getppid()}"
|
|
229
|
+
safe_key = re.sub(r"[^A-Za-z0-9._-]+", "_", context_key).strip("._-")
|
|
230
|
+
if not safe_key:
|
|
231
|
+
safe_key = "session"
|
|
232
|
+
return (
|
|
233
|
+
repo_root
|
|
234
|
+
/ DIR_WORKFLOW
|
|
235
|
+
/ ".runtime"
|
|
236
|
+
/ f"update-check-{safe_key[:160]}.marker"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _mark_update_check_attempted(repo_root: Path) -> bool:
|
|
241
|
+
marker_path = _update_marker_path(repo_root)
|
|
242
|
+
if marker_path.exists():
|
|
243
|
+
return False
|
|
244
|
+
try:
|
|
245
|
+
marker_path.parent.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
marker_path.write_text("checked\n", encoding="utf-8")
|
|
247
|
+
except OSError:
|
|
248
|
+
pass
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _get_update_hint(repo_root: Path) -> str | None:
|
|
253
|
+
marker_path = _update_marker_path(repo_root)
|
|
254
|
+
if marker_path.exists():
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
current_version = _read_project_version(repo_root)
|
|
258
|
+
if not current_version:
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
latest_version = _resolve_available_update_version()
|
|
262
|
+
if not latest_version:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
_mark_update_check_attempted(repo_root)
|
|
266
|
+
comparison = _compare_versions(current_version, latest_version)
|
|
267
|
+
if comparison is None or comparison >= 0:
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
f"Trellis update available: {current_version} -> {latest_version}, "
|
|
272
|
+
f"run npm install -g {_PACKAGE_NAME}@latest"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
112
276
|
# =============================================================================
|
|
113
277
|
# JSON Output
|
|
114
278
|
# =============================================================================
|
|
@@ -571,4 +735,10 @@ def output_text(repo_root: Path | None = None) -> None:
|
|
|
571
735
|
Args:
|
|
572
736
|
repo_root: Repository root path. Defaults to auto-detected.
|
|
573
737
|
"""
|
|
738
|
+
if repo_root is None:
|
|
739
|
+
repo_root = get_repo_root()
|
|
740
|
+
update_hint = _get_update_hint(repo_root)
|
|
741
|
+
if update_hint:
|
|
742
|
+
print(update_hint)
|
|
743
|
+
print("")
|
|
574
744
|
print(get_context_text(repo_root))
|
|
@@ -24,6 +24,7 @@ from pathlib import Path
|
|
|
24
24
|
|
|
25
25
|
from .config import (
|
|
26
26
|
get_packages,
|
|
27
|
+
get_session_auto_commit,
|
|
27
28
|
is_monorepo,
|
|
28
29
|
resolve_package,
|
|
29
30
|
validate_package,
|
|
@@ -391,16 +392,28 @@ def _auto_commit_archive(task_name: str, repo_root: Path) -> None:
|
|
|
391
392
|
"""Stage Trellis-owned task paths and commit after archive.
|
|
392
393
|
|
|
393
394
|
Only stages specific subpaths (the archive subtree and active task dirs),
|
|
394
|
-
never the whole
|
|
395
|
-
|
|
396
|
-
forbids
|
|
395
|
+
never the whole ``.trellis/`` tree. If ``.gitignore`` blocks the paths,
|
|
396
|
+
we warn + skip — we do NOT retry with ``git add -f``. The warning
|
|
397
|
+
explicitly forbids ``git add -f .trellis/`` (which would fan out to
|
|
398
|
+
caches/backups) and points users at ``session_auto_commit: false``.
|
|
399
|
+
|
|
400
|
+
Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when set to
|
|
401
|
+
``false``, this function returns immediately without touching git
|
|
402
|
+
(the archive directory move on disk is unaffected).
|
|
397
403
|
"""
|
|
404
|
+
if not get_session_auto_commit(repo_root):
|
|
405
|
+
print(
|
|
406
|
+
"[OK] session_auto_commit: false — skipping git stage/commit.",
|
|
407
|
+
file=sys.stderr,
|
|
408
|
+
)
|
|
409
|
+
return
|
|
410
|
+
|
|
398
411
|
paths = safe_archive_paths_to_add(repo_root)
|
|
399
412
|
if not paths:
|
|
400
413
|
print("[OK] No task changes to commit.", file=sys.stderr)
|
|
401
414
|
return
|
|
402
415
|
|
|
403
|
-
success,
|
|
416
|
+
success, _, err = safe_git_add(paths, repo_root)
|
|
404
417
|
if not success:
|
|
405
418
|
if err and "ignored by" in err.lower():
|
|
406
419
|
print_gitignore_warning(paths)
|
|
@@ -411,12 +424,6 @@ def _auto_commit_archive(task_name: str, repo_root: Path) -> None:
|
|
|
411
424
|
)
|
|
412
425
|
return
|
|
413
426
|
|
|
414
|
-
if used_force:
|
|
415
|
-
print(
|
|
416
|
-
"[OK] Staged Trellis-owned paths with -f (specific paths, not .trellis/).",
|
|
417
|
-
file=sys.stderr,
|
|
418
|
-
)
|
|
419
|
-
|
|
420
427
|
rc, _, _ = run_git(
|
|
421
428
|
["diff", "--cached", "--quiet", "--", *paths], cwd=repo_root
|
|
422
429
|
)
|
package/package.json
CHANGED