stml-cli 0.1.2__tar.gz → 0.1.4__tar.gz

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 (25) hide show
  1. {stml_cli-0.1.2 → stml_cli-0.1.4}/PKG-INFO +1 -1
  2. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli/__init__.py +1 -1
  3. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli/commands.py +26 -6
  4. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli/packaging.py +7 -1
  5. stml_cli-0.1.4/stml_cli/paths.py +125 -0
  6. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli.egg-info/PKG-INFO +1 -1
  7. stml_cli-0.1.4/tests/test_paths.py +120 -0
  8. stml_cli-0.1.2/stml_cli/paths.py +0 -70
  9. stml_cli-0.1.2/tests/test_paths.py +0 -65
  10. {stml_cli-0.1.2 → stml_cli-0.1.4}/README.md +0 -0
  11. {stml_cli-0.1.2 → stml_cli-0.1.4}/pyproject.toml +0 -0
  12. {stml_cli-0.1.2 → stml_cli-0.1.4}/setup.cfg +0 -0
  13. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli/__main__.py +0 -0
  14. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli/config.py +0 -0
  15. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli/http.py +0 -0
  16. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli/oauth.py +0 -0
  17. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli/refs.py +0 -0
  18. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli.egg-info/SOURCES.txt +0 -0
  19. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli.egg-info/dependency_links.txt +0 -0
  20. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli.egg-info/entry_points.txt +0 -0
  21. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli.egg-info/requires.txt +0 -0
  22. {stml_cli-0.1.2 → stml_cli-0.1.4}/stml_cli.egg-info/top_level.txt +0 -0
  23. {stml_cli-0.1.2 → stml_cli-0.1.4}/tests/test_disambiguation.py +0 -0
  24. {stml_cli-0.1.2 → stml_cli-0.1.4}/tests/test_refs.py +0 -0
  25. {stml_cli-0.1.2 → stml_cli-0.1.4}/tests/test_whoami_expiry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stml-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: The stml partner CLI — pull, edit, push, and publish stml apps and libraries.
5
5
  Author: stml
6
6
  Project-URL: Homepage, https://stml.io
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import os
11
11
 
12
- __version__ = "0.1.2"
12
+ __version__ = "0.1.4"
13
13
 
14
14
  # TLS trust: framework/python.org builds have no OpenSSL CA path, so a real
15
15
  # https backend fails with "unable to get local issuer certificate". certifi's
@@ -70,16 +70,30 @@ def cmd_list(args) -> None:
70
70
  if not apps:
71
71
  _log("No apps found.")
72
72
  return
73
- # Display name + mode (035: attached/detached/scratch) + the app URL — the
74
- # exact handle `stml pull`/`push` accept (the URL's app slug is unique, so
75
- # it never collides even when a library is installed twice). Rows to stdout
76
- # so `stml list | grep …` works; header to stderr.
77
- name_w = max(len(a.get("name") or "") for a in apps)
73
+ # Name + org + workspace + mode (035: attached/detached/scratch) + the app
74
+ # URL — the exact handle `stml pull`/`push` accept (the URL's app slug is
75
+ # unique, so it never collides even when a library is installed twice). Org
76
+ # and workspace disambiguate same-named apps across the account. Names fall
77
+ # back to slugs against older backends. Rows to stdout so `stml list | grep …`
78
+ # works; header to stderr.
79
+ def _org(a):
80
+ return a.get("org_name") or a.get("org_slug") or ""
81
+
82
+ def _ws(a):
83
+ return a.get("workspace_name") or a.get("workspace_slug") or ""
84
+
85
+ name_w = max((len(a.get("name") or "") for a in apps), default=3)
86
+ org_w = max((len(_org(a)) for a in apps), default=3)
87
+ ws_w = max((len(_ws(a)) for a in apps), default=2)
78
88
  mode_w = max((len(a.get("mode") or "") for a in apps), default=4)
79
- _log(f"{'APP'.ljust(name_w)} {'MODE'.ljust(mode_w)} URL")
89
+ _log(
90
+ f"{'APP'.ljust(name_w)} {'ORG'.ljust(org_w)} "
91
+ f"{'WORKSPACE'.ljust(ws_w)} {'MODE'.ljust(mode_w)} URL"
92
+ )
80
93
  for a in apps:
81
94
  print(
82
95
  f"{(a.get('name') or '').ljust(name_w)} "
96
+ f"{_org(a).ljust(org_w)} {_ws(a).ljust(ws_w)} "
83
97
  f"{(a.get('mode') or '').ljust(mode_w)} {a.get('url') or ''}"
84
98
  )
85
99
 
@@ -238,6 +252,12 @@ def cmd_publish(args) -> None:
238
252
  if not org_slug:
239
253
  raise SystemExit("Pass --org or set [tool.stml].issuer-org in pyproject.toml")
240
254
 
255
+ # Pre-flight: show the resolved coordinate BEFORE uploading, and say where
256
+ # the issuer came from — a pulled fork silently carries the ORIGINAL
257
+ # issuer-org in its pyproject, which is exactly when you want `--org`.
258
+ origin = "--org" if args.org else "[tool.stml].issuer-org in pyproject.toml"
259
+ _log(f"Publishing as {org_slug}/{name}@{version} (issuer from {origin})")
260
+
241
261
  token = http.resolve_token(args.backend)
242
262
 
243
263
  # Ensure the library exists (409 = already there, which is fine).
@@ -4,6 +4,12 @@ One directory → a ``<dir-name>/…`` gzip tarball, excluding local dev junk. T
4
4
  platform publish endpoint reads the manifest and explodes ``src/``; this only
5
5
  packages the bytes. Kept byte-compatible with the monorepo builder so a lib
6
6
  published by either path lands identically.
7
+
8
+ The publish process deliberately stays layout-strict (src/ required for
9
+ runnable Python — story 020's guard): a folder made by ``stml pull`` is
10
+ ALREADY written in the src/ library layout (the overlay's flat keys are a
11
+ platform-internal storage detail — see ``paths.write_tree``), so a pulled
12
+ fork publishes here without repackaging.
7
13
  """
8
14
 
9
15
  from __future__ import annotations
@@ -13,7 +19,7 @@ import tarfile
13
19
  from pathlib import Path
14
20
 
15
21
  EXCLUDE_DIRS = {
16
- ".git", ".venv", "venv", "__pycache__", "node_modules", ".pytest_cache",
22
+ ".git", ".stml", ".venv", "venv", "__pycache__", "node_modules", ".pytest_cache",
17
23
  "dist", "build", ".mypy_cache", ".ruff_cache",
18
24
  # Unit tests live in a top-level tests/ per lib (pytest testpaths); test-
19
25
  # named files inside packages are intentional runnable flows, so dir-only.
@@ -0,0 +1,125 @@
1
+ """Cross-platform overlay ↔ local-folder mapping (024 §OS & runtime support).
2
+
3
+ The overlay keys the platform stores are always POSIX (``flows/x.py``). "Runs on
4
+ Windows" is only real if the CLI:
5
+ 1. maps a POSIX key to a NATIVE path when writing to disk, and back to a POSIX
6
+ key (never backslashes) when reading — via ``PurePosixPath`` on the wire,
7
+ ``Path`` on disk;
8
+ 2. reads/writes UTF-8 and preserves ``\n`` exactly, so a pulled file survives
9
+ an untouched push byte-for-byte (no CRLF injection on Windows).
10
+
11
+ Layout mapping (authoring vs deploying): on DISK the CLI presents the standard
12
+ **src/ library layout** — the same shape as a real library repo, what
13
+ ``stml publish`` expects, and what a human authors. The platform's overlay
14
+ stores the FLAT deploy-side keys (``pkg/flow.py``); that is an internal storage
15
+ detail that must not leak to disk. So:
16
+
17
+ * ``write_tree`` (pull) — a flat overlay with Python is written with its
18
+ runnable files under ``src/`` (packaging/metadata — pyproject, top-level
19
+ ``*.md``, ``page/**``, LICENSE — stays at the root, mirroring the publish
20
+ tarball layout and the server's ``_is_packaging_path``).
21
+ * ``read_tree`` (push) — a folder with a top-level ``src/`` has that prefix
22
+ stripped back to the flat overlay keys, so an overlay row always shadows
23
+ its base-library path exactly. A flat folder (pre-mapping pulls, hand-made
24
+ trees) passes through unchanged.
25
+
26
+ The two directions are inverse by construction, so pull → push is lossless.
27
+
28
+ These functions are the whole contract; they are unit-tested in isolation so
29
+ the Windows guarantee doesn't rest on a live round-trip.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from pathlib import Path, PurePosixPath
35
+
36
+
37
+ def _is_packaging_path(rel: str) -> bool:
38
+ """Paths that live at the ROOT in the src-layout view (not under ``src/``) —
39
+ metadata the publish endpoint reads there and the deploy bundler ignores.
40
+ Must match the server's ``_is_packaging_path`` so the disk layout equals
41
+ the publish-tarball layout."""
42
+ if rel in ("pyproject.toml", "README.md", "LICENSE", "py.typed"):
43
+ return True
44
+ if rel.startswith("page/"): # app-page assets, served separately
45
+ return True
46
+ if "/" not in rel and rel.endswith(".md"): # top-level docs incl. AGENTS.md
47
+ return True
48
+ return False
49
+
50
+ # Local-only artifacts that must never be pushed into the overlay.
51
+ _EXCLUDE_DIRS = {
52
+ ".git", "__pycache__", ".venv", "venv", ".stml",
53
+ "node_modules", ".mypy_cache", ".ruff_cache", ".pytest_cache",
54
+ }
55
+ _EXCLUDE_NAMES = {".DS_Store"}
56
+ _EXCLUDE_SUFFIXES = {".pyc", ".pyo"}
57
+
58
+
59
+ def to_posix_key(rel: Path) -> str:
60
+ """A native relative path → the POSIX overlay key (forward slashes)."""
61
+ return PurePosixPath(*rel.parts).as_posix()
62
+
63
+
64
+ def key_to_native(key: str) -> Path:
65
+ """A POSIX overlay key → a native relative Path for this OS."""
66
+ return Path(*PurePosixPath(key).parts)
67
+
68
+
69
+ def read_tree(root: Path) -> list[dict]:
70
+ """Read a local folder into ``[{path, content}]`` with POSIX overlay keys.
71
+
72
+ A top-level ``src/`` (the on-disk library layout ``write_tree`` produces)
73
+ is stripped back to the flat overlay keys — the canonical form the platform
74
+ stores, and the form in which an overlay row shadows its base-library path.
75
+ A same-key collision (``src/pkg/x.py`` next to ``pkg/x.py``) is ambiguous
76
+ and raises rather than silently picking a winner.
77
+
78
+ ``read_text`` (universal newlines) collapses any CRLF to ``\n``, so the
79
+ content pushed is always LF-normalised regardless of the author's OS.
80
+ """
81
+ out: list[dict] = []
82
+ seen: dict[str, str] = {} # overlay key → original rel (collision detection)
83
+ for p in sorted(root.rglob("*")):
84
+ rel = p.relative_to(root)
85
+ if any(part in _EXCLUDE_DIRS for part in rel.parts):
86
+ continue
87
+ if p.name in _EXCLUDE_NAMES or p.suffix in _EXCLUDE_SUFFIXES:
88
+ continue
89
+ if not p.is_file():
90
+ continue
91
+ key = to_posix_key(rel)
92
+ if key.startswith("src/"):
93
+ key = key[len("src/"):]
94
+ if key in seen:
95
+ raise ValueError(
96
+ f"ambiguous tree: {rel.as_posix()!r} and {seen[key]!r} both map to "
97
+ f"overlay key {key!r} — keep runnable code under src/ only."
98
+ )
99
+ seen[key] = rel.as_posix()
100
+ out.append({"path": key, "content": p.read_text(encoding="utf-8")})
101
+ return out
102
+
103
+
104
+ def write_tree(root: Path, files: list[dict]) -> list[str]:
105
+ """Write ``[{path, content}]`` (POSIX overlay keys) into a local folder in
106
+ the standard **src/ library layout**: when the overlay carries Python (and
107
+ isn't already src-laid-out), runnable files land under ``src/`` and
108
+ packaging/metadata stays at the root — so the folder is a valid library
109
+ directory (`stml publish` accepts it as-is) and diffs cleanly against a
110
+ real library repo. Returns the overlay keys written (not disk paths).
111
+
112
+ ``newline=""`` disables newline translation so ``\n`` is written verbatim —
113
+ a pulled file is not silently rewritten to CRLF on Windows.
114
+ """
115
+ py_keys = [f["path"] for f in files if f["path"].endswith(".py")]
116
+ relocate = bool(py_keys) and not any(k.startswith("src/") for k in py_keys)
117
+ written: list[str] = []
118
+ for f in files:
119
+ key = f["path"]
120
+ disk_key = f"src/{key}" if relocate and not _is_packaging_path(key) else key
121
+ dest = root / key_to_native(disk_key)
122
+ dest.parent.mkdir(parents=True, exist_ok=True)
123
+ dest.write_text(f.get("content") or "", encoding="utf-8", newline="")
124
+ written.append(key)
125
+ return written
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stml-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: The stml partner CLI — pull, edit, push, and publish stml apps and libraries.
5
5
  Author: stml
6
6
  Project-URL: Homepage, https://stml.io
@@ -0,0 +1,120 @@
1
+ """Cross-platform overlay ↔ folder mapping (024 acceptance: CLI cross-platform).
2
+
3
+ The Windows guarantee ("a pulled tree round-trips; overlay keys stay POSIX and
4
+ content stays UTF-8/\\n") rests entirely on paths.read_tree / write_tree, so
5
+ they are pinned here in isolation — no live backend, no OS assumptions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ from stml_cli import paths
13
+
14
+
15
+ def test_keys_are_posix_even_for_nested_paths(tmp_path: Path):
16
+ files = [
17
+ {"path": "flows/greeting.py", "content": "print('hi')\n"},
18
+ {"path": "page/index.html", "content": "<h1>hi</h1>\n"},
19
+ {"path": "pyproject.toml", "content": "[project]\n"},
20
+ ]
21
+ paths.write_tree(tmp_path, files)
22
+ # On disk: the standard src/ library layout — runnable Python under src/,
23
+ # packaging (page/, pyproject) at the root (authoring vs deploying: the
24
+ # flat overlay key is a platform-internal storage detail).
25
+ assert (tmp_path / "src" / "flows" / "greeting.py").is_file()
26
+ assert (tmp_path / "page" / "index.html").is_file()
27
+ assert (tmp_path / "pyproject.toml").is_file()
28
+ # ...but reading back yields the flat forward-slash overlay keys.
29
+ keys = {f["path"] for f in paths.read_tree(tmp_path)}
30
+ assert keys == {"flows/greeting.py", "page/index.html", "pyproject.toml"}
31
+ assert not any("\\" in k for k in keys)
32
+
33
+
34
+ def test_page_only_overlay_stays_flat_on_disk(tmp_path: Path):
35
+ """No Python in the overlay (fresh attached install) → nothing to wrap."""
36
+ paths.write_tree(tmp_path, [{"path": "page/index.html", "content": "<h1/>\n"}])
37
+ assert (tmp_path / "page" / "index.html").is_file()
38
+ assert not (tmp_path / "src").exists()
39
+
40
+
41
+ def test_src_layout_folder_pushes_flat_keys(tmp_path: Path):
42
+ """A real library folder (hand-made or pulled) pushes the FLAT overlay keys,
43
+ so an overlay row shadows its base-library path exactly."""
44
+ (tmp_path / "src" / "pkg").mkdir(parents=True)
45
+ (tmp_path / "src" / "pkg" / "x.py").write_text("x = 1\n")
46
+ (tmp_path / "pyproject.toml").write_text("[project]\n")
47
+ keys = {f["path"] for f in paths.read_tree(tmp_path)}
48
+ assert keys == {"pkg/x.py", "pyproject.toml"}
49
+
50
+
51
+ def test_flat_folder_passes_through_unchanged(tmp_path: Path):
52
+ """Pre-mapping pulls / hand-made flat trees keep working (no src/ → no strip)."""
53
+ (tmp_path / "pkg").mkdir()
54
+ (tmp_path / "pkg" / "x.py").write_text("x = 1\n")
55
+ keys = {f["path"] for f in paths.read_tree(tmp_path)}
56
+ assert keys == {"pkg/x.py"}
57
+
58
+
59
+ def test_pull_push_round_trip_is_lossless(tmp_path: Path):
60
+ files = [
61
+ {"path": "pkg/flow.py", "content": "F = 1\n"},
62
+ {"path": "pkg/data.csv", "content": "a,b\n"},
63
+ {"path": "page/index.html", "content": "<h1/>\n"},
64
+ {"path": "AGENTS.md", "content": "# doc\n"},
65
+ {"path": "pyproject.toml", "content": "[project]\n"},
66
+ ]
67
+ paths.write_tree(tmp_path, files)
68
+ got = sorted(paths.read_tree(tmp_path), key=lambda f: f["path"])
69
+ assert got == sorted(files, key=lambda f: f["path"])
70
+
71
+
72
+ def test_ambiguous_src_and_flat_collision_raises(tmp_path: Path):
73
+ (tmp_path / "src" / "pkg").mkdir(parents=True)
74
+ (tmp_path / "src" / "pkg" / "x.py").write_text("a = 1\n")
75
+ (tmp_path / "pkg").mkdir()
76
+ (tmp_path / "pkg" / "x.py").write_text("b = 2\n")
77
+ try:
78
+ paths.read_tree(tmp_path)
79
+ raise AssertionError("expected ValueError on ambiguous tree")
80
+ except ValueError as e:
81
+ assert "ambiguous" in str(e)
82
+
83
+
84
+ def test_content_round_trips_lf_exactly(tmp_path: Path):
85
+ original = [{"path": "flows/x.py", "content": "a = 1\nb = 2\n"}]
86
+ paths.write_tree(tmp_path, original)
87
+ # Written verbatim — no CRLF injection, even on Windows (newline="").
88
+ # Runnable Python lands under src/ (the on-disk library layout).
89
+ raw = (tmp_path / "src" / "flows" / "x.py").read_bytes()
90
+ assert b"\r\n" not in raw
91
+ assert raw == b"a = 1\nb = 2\n"
92
+ # And read_tree returns the same LF content under the flat overlay key.
93
+ assert paths.read_tree(tmp_path)[0]["content"] == "a = 1\nb = 2\n"
94
+
95
+
96
+ def test_crlf_on_disk_is_normalised_to_lf_on_read(tmp_path: Path):
97
+ # Simulate a file that somehow has CRLF on disk (e.g. a Windows editor).
98
+ (tmp_path / "flows").mkdir()
99
+ (tmp_path / "flows" / "y.py").write_bytes(b"a = 1\r\nb = 2\r\n")
100
+ got = {f["path"]: f["content"] for f in paths.read_tree(tmp_path)}
101
+ assert got["flows/y.py"] == "a = 1\nb = 2\n" # pushed content is LF-normalised
102
+
103
+
104
+ def test_local_junk_is_excluded(tmp_path: Path):
105
+ paths.write_tree(tmp_path, [{"path": "flows/x.py", "content": "x=1\n"}])
106
+ (tmp_path / ".git").mkdir()
107
+ (tmp_path / ".git" / "config").write_text("[core]\n")
108
+ (tmp_path / "__pycache__").mkdir()
109
+ (tmp_path / "__pycache__" / "x.pyc").write_bytes(b"\x00")
110
+ (tmp_path / ".stml").mkdir()
111
+ (tmp_path / ".stml" / "app.json").write_text("{}")
112
+ (tmp_path / "src" / "flows" / "x.pyc").write_bytes(b"\x00")
113
+ keys = {f["path"] for f in paths.read_tree(tmp_path)}
114
+ assert keys == {"flows/x.py"} # .git, __pycache__, .stml, *.pyc all dropped
115
+
116
+
117
+ def test_key_to_native_and_back():
118
+ native = paths.key_to_native("flows/sub/x.py")
119
+ assert native == Path("flows") / "sub" / "x.py"
120
+ assert paths.to_posix_key(native) == "flows/sub/x.py"
@@ -1,70 +0,0 @@
1
- """Cross-platform overlay ↔ local-folder mapping (024 §OS & runtime support).
2
-
3
- The overlay keys the platform stores are always POSIX (``flows/x.py``). "Runs on
4
- Windows" is only real if the CLI:
5
- 1. maps a POSIX key to a NATIVE path when writing to disk, and back to a POSIX
6
- key (never backslashes) when reading — via ``PurePosixPath`` on the wire,
7
- ``Path`` on disk;
8
- 2. reads/writes UTF-8 and preserves ``\n`` exactly, so a pulled file survives
9
- an untouched push byte-for-byte (no CRLF injection on Windows).
10
-
11
- These two functions are the whole contract; they are unit-tested in isolation so
12
- the Windows guarantee doesn't rest on a live round-trip.
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- from pathlib import Path, PurePosixPath
18
-
19
- # Local-only artifacts that must never be pushed into the overlay.
20
- _EXCLUDE_DIRS = {
21
- ".git", "__pycache__", ".venv", "venv", ".stml",
22
- "node_modules", ".mypy_cache", ".ruff_cache", ".pytest_cache",
23
- }
24
- _EXCLUDE_NAMES = {".DS_Store"}
25
- _EXCLUDE_SUFFIXES = {".pyc", ".pyo"}
26
-
27
-
28
- def to_posix_key(rel: Path) -> str:
29
- """A native relative path → the POSIX overlay key (forward slashes)."""
30
- return PurePosixPath(*rel.parts).as_posix()
31
-
32
-
33
- def key_to_native(key: str) -> Path:
34
- """A POSIX overlay key → a native relative Path for this OS."""
35
- return Path(*PurePosixPath(key).parts)
36
-
37
-
38
- def read_tree(root: Path) -> list[dict]:
39
- """Read a local folder into ``[{path, content}]`` with POSIX keys.
40
-
41
- ``read_text`` (universal newlines) collapses any CRLF to ``\n``, so the
42
- content pushed is always LF-normalised regardless of the author's OS.
43
- """
44
- out: list[dict] = []
45
- for p in sorted(root.rglob("*")):
46
- rel = p.relative_to(root)
47
- if any(part in _EXCLUDE_DIRS for part in rel.parts):
48
- continue
49
- if p.name in _EXCLUDE_NAMES or p.suffix in _EXCLUDE_SUFFIXES:
50
- continue
51
- if not p.is_file():
52
- continue
53
- out.append({"path": to_posix_key(rel), "content": p.read_text(encoding="utf-8")})
54
- return out
55
-
56
-
57
- def write_tree(root: Path, files: list[dict]) -> list[str]:
58
- """Write ``[{path, content}]`` (POSIX keys) into a local folder.
59
-
60
- ``newline=""`` disables newline translation so ``\n`` is written verbatim —
61
- a pulled file is not silently rewritten to CRLF on Windows.
62
- """
63
- written: list[str] = []
64
- for f in files:
65
- key = f["path"]
66
- dest = root / key_to_native(key)
67
- dest.parent.mkdir(parents=True, exist_ok=True)
68
- dest.write_text(f.get("content") or "", encoding="utf-8", newline="")
69
- written.append(key)
70
- return written
@@ -1,65 +0,0 @@
1
- """Cross-platform overlay ↔ folder mapping (024 acceptance: CLI cross-platform).
2
-
3
- The Windows guarantee ("a pulled tree round-trips; overlay keys stay POSIX and
4
- content stays UTF-8/\\n") rests entirely on paths.read_tree / write_tree, so
5
- they are pinned here in isolation — no live backend, no OS assumptions.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from pathlib import Path
11
-
12
- from stml_cli import paths
13
-
14
-
15
- def test_keys_are_posix_even_for_nested_paths(tmp_path: Path):
16
- files = [
17
- {"path": "flows/greeting.py", "content": "print('hi')\n"},
18
- {"path": "page/index.html", "content": "<h1>hi</h1>\n"},
19
- {"path": "pyproject.toml", "content": "[project]\n"},
20
- ]
21
- paths.write_tree(tmp_path, files)
22
- # Nested folders are created natively on disk...
23
- assert (tmp_path / "flows" / "greeting.py").is_file()
24
- # ...but reading back yields forward-slash POSIX keys, never backslashes.
25
- keys = {f["path"] for f in paths.read_tree(tmp_path)}
26
- assert keys == {"flows/greeting.py", "page/index.html", "pyproject.toml"}
27
- assert not any("\\" in k for k in keys)
28
-
29
-
30
- def test_content_round_trips_lf_exactly(tmp_path: Path):
31
- original = [{"path": "flows/x.py", "content": "a = 1\nb = 2\n"}]
32
- paths.write_tree(tmp_path, original)
33
- # Written verbatim — no CRLF injection, even on Windows (newline="").
34
- raw = (tmp_path / "flows" / "x.py").read_bytes()
35
- assert b"\r\n" not in raw
36
- assert raw == b"a = 1\nb = 2\n"
37
- # And read_tree returns the same LF content.
38
- assert paths.read_tree(tmp_path)[0]["content"] == "a = 1\nb = 2\n"
39
-
40
-
41
- def test_crlf_on_disk_is_normalised_to_lf_on_read(tmp_path: Path):
42
- # Simulate a file that somehow has CRLF on disk (e.g. a Windows editor).
43
- (tmp_path / "flows").mkdir()
44
- (tmp_path / "flows" / "y.py").write_bytes(b"a = 1\r\nb = 2\r\n")
45
- got = {f["path"]: f["content"] for f in paths.read_tree(tmp_path)}
46
- assert got["flows/y.py"] == "a = 1\nb = 2\n" # pushed content is LF-normalised
47
-
48
-
49
- def test_local_junk_is_excluded(tmp_path: Path):
50
- paths.write_tree(tmp_path, [{"path": "flows/x.py", "content": "x=1\n"}])
51
- (tmp_path / ".git").mkdir()
52
- (tmp_path / ".git" / "config").write_text("[core]\n")
53
- (tmp_path / "__pycache__").mkdir()
54
- (tmp_path / "__pycache__" / "x.pyc").write_bytes(b"\x00")
55
- (tmp_path / ".stml").mkdir()
56
- (tmp_path / ".stml" / "app.json").write_text("{}")
57
- (tmp_path / "flows" / "x.pyc").write_bytes(b"\x00")
58
- keys = {f["path"] for f in paths.read_tree(tmp_path)}
59
- assert keys == {"flows/x.py"} # .git, __pycache__, .stml, *.pyc all dropped
60
-
61
-
62
- def test_key_to_native_and_back():
63
- native = paths.key_to_native("flows/sub/x.py")
64
- assert native == Path("flows") / "sub" / "x.py"
65
- assert paths.to_posix_key(native) == "flows/sub/x.py"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes