xgic-payload-cms-cli 0.2.3__tar.gz → 0.2.5__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 (18) hide show
  1. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/PKG-INFO +2 -2
  2. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/README.md +1 -1
  3. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/pyproject.toml +1 -1
  4. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/__init__.py +1 -1
  5. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/commands/reset.py +30 -10
  6. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/config.py +5 -1
  7. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/env_helpers.py +5 -0
  8. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/layout.py +6 -1
  9. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/project.py +77 -11
  10. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/.gitignore +0 -0
  11. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/LICENSE +0 -0
  12. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/NOTICE +0 -0
  13. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/commands/__init__.py +0 -0
  14. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/commands/dev.py +0 -0
  15. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/commands/payload_env.py +0 -0
  16. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/commands/schema.py +0 -0
  17. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/commands/setup.py +0 -0
  18. {xgic_payload_cms_cli-0.2.3 → xgic_payload_cms_cli-0.2.5}/src/xgic/cli/payload/plugin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: xgic-payload-cms-cli
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: XGIC CLI Payload CMS module - product commands and helpers (xgic.cli.payload).
5
5
  Project-URL: Homepage, https://github.com/xgic/payload-cms-cli
6
6
  Project-URL: Repository, https://github.com/xgic/payload-cms-cli
@@ -90,7 +90,7 @@ One brand (`xgic`), clear ownership, and dry-run-friendly destructive paths.
90
90
  uv pip install \
91
91
  "xgic-cli>=0.2.0,<0.3" \
92
92
  "xgic-dev-cli>=0.2.0,<0.3" \
93
- "xgic-payload-cms-cli>=0.2.3,<0.3"
93
+ "xgic-payload-cms-cli>=0.2.5,<0.3"
94
94
  xgic payload --help
95
95
  ```
96
96
 
@@ -55,7 +55,7 @@ One brand (`xgic`), clear ownership, and dry-run-friendly destructive paths.
55
55
  uv pip install \
56
56
  "xgic-cli>=0.2.0,<0.3" \
57
57
  "xgic-dev-cli>=0.2.0,<0.3" \
58
- "xgic-payload-cms-cli>=0.2.3,<0.3"
58
+ "xgic-payload-cms-cli>=0.2.5,<0.3"
59
59
  xgic payload --help
60
60
  ```
61
61
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "xgic-payload-cms-cli"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "XGIC CLI Payload CMS module - product commands and helpers (xgic.cli.payload)."
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -21,7 +21,7 @@ from xgic.cli.payload.project import (
21
21
  load_create_payload_config,
22
22
  )
23
23
 
24
- __version__ = "0.2.3"
24
+ __version__ = "0.2.5"
25
25
 
26
26
  __all__ = [
27
27
  "DEFAULT_COMPOSE_PROJECT",
@@ -3,23 +3,31 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import shutil
6
- from pathlib import Path
7
6
 
8
7
  from xgic.cli.app import CommandContext
9
8
  from xgic.cli.payload.config import (
10
9
  get_db_config,
11
10
  get_db_profile,
12
- get_payload_project_name,
13
11
  make_payload_docker_controller,
14
12
  )
15
13
  from xgic.cli.payload.env_helpers import perform_env_regenerate
16
- from xgic.cli.payload.project import ensure_payload_project
14
+ from xgic.cli.payload.layout import (
15
+ display_project_location,
16
+ is_workspace_root,
17
+ resolve_project_dir,
18
+ )
19
+ from xgic.cli.payload.project import (
20
+ ensure_payload_project,
21
+ load_create_payload_config,
22
+ )
17
23
  from xgic.cli.utils.output import print_info, print_success, print_warning
18
24
 
19
25
 
20
26
  def run_reset(ctx: CommandContext) -> int:
21
27
  """Delete generated project folder and reset the active DB volume.
22
28
 
29
+ Uses ``projectDir`` (same resolver as setup), not ``projectName``.
30
+ App-root layout (workspace ``.``) is never ``rmtree``'d.
23
31
  Credentials in ``.env`` are left alone unless ``--rotate-credentials``.
24
32
  """
25
33
  dry_run = bool(getattr(ctx.args, "dry_run", False))
@@ -27,13 +35,20 @@ def run_reset(ctx: CommandContext) -> int:
27
35
  rotate = bool(getattr(ctx.args, "rotate_credentials", False))
28
36
 
29
37
  docker = make_payload_docker_controller(ctx.env)
30
- payload_project = get_payload_project_name()
31
- project_path = Path(payload_project)
38
+ project_dir = resolve_project_dir(load_create_payload_config())
39
+ skip_dir = is_workspace_root(project_dir)
40
+ location = display_project_location(project_dir)
32
41
  db_service = get_db_profile()
33
42
  db_volume = docker.db_volume_name(db_service)
34
43
 
35
44
  print_info("Planned actions for reset:")
36
- print_info(f" - Delete directory: {project_path}")
45
+ if skip_dir:
46
+ print_info(
47
+ " - Skip project directory delete "
48
+ f"({location} is the workspace; not removing .git / .devcontainer)"
49
+ )
50
+ else:
51
+ print_info(f" - Delete directory: {location}")
37
52
  print_info(f" - Remove Docker volume: {db_volume}")
38
53
  if rotate:
39
54
  print_warning(" - ALSO rotate database credentials (DANGEROUS)")
@@ -50,11 +65,16 @@ def run_reset(ctx: CommandContext) -> int:
50
65
 
51
66
  print_info("Performing reset...")
52
67
 
53
- if project_path.exists():
54
- shutil.rmtree(project_path)
55
- print_success(f"Deleted project directory: {project_path}")
68
+ if skip_dir:
69
+ print_info(
70
+ "App-root layout: left workspace files in place. "
71
+ "Remove generated app sources manually if you need a clean scaffold."
72
+ )
73
+ elif project_dir.exists():
74
+ shutil.rmtree(project_dir)
75
+ print_success(f"Deleted project directory: {location}")
56
76
  else:
57
- print_info(f"Project directory {project_path} did not exist.")
77
+ print_info(f"Project directory {location} did not exist.")
58
78
 
59
79
  docker.rm_service(db_service, force=True, stop=True, remove_volumes=False)
60
80
 
@@ -191,8 +191,10 @@ def db_ready(
191
191
  except Exception:
192
192
  return False
193
193
 
194
- _, db_user = get_db_config(config_file)
194
+ db_name, db_user = get_db_config(config_file)
195
195
  try:
196
+ # -d is required: libpq defaults the database name to the user
197
+ # (POSTGRES_USER=payload) while POSTGRES_DB is payload_db.
196
198
  result = docker._run_compose( # noqa: SLF001
197
199
  "exec",
198
200
  "-T",
@@ -200,6 +202,8 @@ def db_ready(
200
202
  "pg_isready",
201
203
  "-U",
202
204
  db_user,
205
+ "-d",
206
+ db_name,
203
207
  capture_output=True,
204
208
  check=False,
205
209
  )
@@ -53,9 +53,14 @@ DATABASE_URL=mongodb://{db_user}:{mongo_pass}@mongodb:27017/{db_name}?authSource
53
53
  """
54
54
 
55
55
  pg_pass = secrets.token_hex(16)
56
+ # PGUSER/PGDATABASE are libpq defaults. Without them, `pg_isready -U payload`
57
+ # and other clients open a database named after the user (`payload`) while
58
+ # POSTGRES_DB is `payload_db`.
56
59
  return f"""POSTGRES_USER={db_user}
57
60
  POSTGRES_PASSWORD={pg_pass}
58
61
  POSTGRES_DB={db_name}
62
+ PGUSER={db_user}
63
+ PGDATABASE={db_name}
59
64
  PAYLOAD_SECRET={payload_secret}
60
65
  DATABASE_URL=postgres://{db_user}:{pg_pass}@postgres:5432/{db_name}
61
66
  """
@@ -83,8 +83,13 @@ def create_payload_target_arg(project_dir: Path) -> str:
83
83
  return project_dir.as_posix().strip("/")
84
84
 
85
85
 
86
+ def is_workspace_root(project_dir: Path) -> bool:
87
+ """Return True when *project_dir* is the workspace root (app-root layout)."""
88
+ return project_dir == Path(".") or str(project_dir).strip() in (".", "./", "")
89
+
90
+
86
91
  def display_project_location(project_dir: Path) -> str:
87
92
  """Human-readable location for logs."""
88
- if project_dir == Path("."):
93
+ if is_workspace_root(project_dir):
89
94
  return "workspace root (.)"
90
95
  return f"{project_dir.as_posix()}/"
@@ -100,6 +100,25 @@ def pnpx_allow_build_args(
100
100
  return [f"--allow-build={name}" for name in packages]
101
101
 
102
102
 
103
+ def detect_pnpm_major() -> int | None:
104
+ """Return the major version of ``pnpm`` on PATH, or None if unknown."""
105
+ try:
106
+ result = subprocess.run(
107
+ ["pnpm", "--version"],
108
+ check=False,
109
+ capture_output=True,
110
+ text=True,
111
+ )
112
+ except OSError:
113
+ return None
114
+ if result.returncode != 0:
115
+ return None
116
+ first = result.stdout.strip().split(".", 1)[0]
117
+ if first.isdigit():
118
+ return int(first)
119
+ return None
120
+
121
+
103
122
  def merge_package_json_only_built(
104
123
  text: str, names: tuple[str, ...] = NATIVE_PNPM_BUILDS
105
124
  ) -> str:
@@ -117,6 +136,41 @@ def merge_package_json_only_built(
117
136
  return json.dumps(data, indent=2) + "\n"
118
137
 
119
138
 
139
+ def strip_package_json_only_built(text: str) -> str:
140
+ """Remove ``pnpm.onlyBuiltDependencies`` (pnpm 11+ ignores that field)."""
141
+ data = json.loads(text)
142
+ pnpm = data.get("pnpm")
143
+ if not isinstance(pnpm, dict) or "onlyBuiltDependencies" not in pnpm:
144
+ return text
145
+ pnpm.pop("onlyBuiltDependencies", None)
146
+ if pnpm:
147
+ data["pnpm"] = pnpm
148
+ else:
149
+ data.pop("pnpm", None)
150
+ return json.dumps(data, indent=2) + "\n"
151
+
152
+
153
+ def yaml_map_key(name: str) -> str:
154
+ """Quote YAML mapping keys that are not plain scalars (e.g. ``@swc/core``)."""
155
+ if not name or name[0] in "@`&*!|>%{}[]'\"" or any(
156
+ ch in name for ch in ":#"
157
+ ):
158
+ return json.dumps(name)
159
+ return name
160
+
161
+
162
+ def parse_yaml_map_key(raw: str) -> str:
163
+ """Strip optional YAML quotes from a mapping key token."""
164
+ raw = raw.strip()
165
+ if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in "\"'":
166
+ return raw[1:-1]
167
+ return raw
168
+
169
+
170
+ def _allow_builds_block(names: list[str]) -> list[str]:
171
+ return ["allowBuilds:"] + [f" {yaml_map_key(n)}: true" for n in names]
172
+
173
+
120
174
  def merge_workspace_allow_builds(
121
175
  text: str, names: tuple[str, ...] = NATIVE_PNPM_BUILDS
122
176
  ) -> str:
@@ -124,6 +178,7 @@ def merge_workspace_allow_builds(
124
178
 
125
179
  Avoids a YAML dependency: only handles the generated website-template
126
180
  shape (a top-level ``allowBuilds:`` mapping of ``name: true``).
181
+ Always re-emits the block so unquoted ``@`` keys from 0.2.3 are repaired.
127
182
  """
128
183
  normalized = text if text.endswith("\n") or text == "" else text + "\n"
129
184
  lines = normalized.splitlines()
@@ -132,7 +187,7 @@ def merge_workspace_allow_builds(
132
187
  None,
133
188
  )
134
189
  if start is None:
135
- extra = ["allowBuilds:"] + [f" {n}: true" for n in names]
190
+ extra = _allow_builds_block(list(names))
136
191
  return normalized + "\n".join(extra) + "\n"
137
192
 
138
193
  existing: list[str] = []
@@ -143,35 +198,46 @@ def merge_workspace_allow_builds(
143
198
  end += 1
144
199
  continue
145
200
  if ln.startswith(" ") or ln.startswith("\t"):
146
- existing.append(ln.strip().split(":", 1)[0].strip())
201
+ key_raw = ln.strip().split(":", 1)[0]
202
+ existing.append(parse_yaml_map_key(key_raw))
147
203
  end += 1
148
204
  continue
149
205
  break
150
- missing = [n for n in names if n not in existing]
151
- if not missing:
152
- return normalized
153
- insert = [f" {n}: true" for n in missing]
154
- new_lines = lines[:end] + insert + lines[end:]
206
+ merged = list(dict.fromkeys([*existing, *names]))
207
+ block = _allow_builds_block(merged)
208
+ new_lines = lines[:start] + block + lines[end:]
155
209
  return "\n".join(new_lines) + "\n"
156
210
 
157
211
 
158
212
  def ensure_native_pnpm_builds(
159
213
  project_dir: Path, *, quiet: bool = False
160
214
  ) -> None:
161
- """Allow-list native install scripts in a generated Payload CMS app."""
215
+ """Allow-list native install scripts in a generated Payload CMS app.
216
+
217
+ pnpm 10 reads ``package.json#pnpm.onlyBuiltDependencies``. pnpm 11+
218
+ ignores that field and uses ``pnpm-workspace.yaml`` ``allowBuilds``.
219
+ """
162
220
  changed = False
163
221
  workspace = project_dir / "pnpm-workspace.yaml"
164
222
  package_json = project_dir / "package.json"
223
+ major = detect_pnpm_major()
165
224
  try:
166
- if workspace.is_file():
167
- old = workspace.read_text(encoding="utf-8")
225
+ if workspace.is_file() or (major is not None and major >= 11):
226
+ old = (
227
+ workspace.read_text(encoding="utf-8")
228
+ if workspace.is_file()
229
+ else ""
230
+ )
168
231
  new = merge_workspace_allow_builds(old)
169
232
  if new != old:
170
233
  workspace.write_text(new, encoding="utf-8")
171
234
  changed = True
172
235
  if package_json.is_file():
173
236
  old = package_json.read_text(encoding="utf-8")
174
- new = merge_package_json_only_built(old)
237
+ if major is not None and major >= 11:
238
+ new = strip_package_json_only_built(old)
239
+ else:
240
+ new = merge_package_json_only_built(old)
175
241
  if new != old:
176
242
  package_json.write_text(new, encoding="utf-8")
177
243
  changed = True