codexspec 0.6.3__tar.gz → 0.6.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 (57) hide show
  1. {codexspec-0.6.3 → codexspec-0.6.4}/PKG-INFO +1 -1
  2. {codexspec-0.6.3 → codexspec-0.6.4}/pyproject.toml +1 -1
  3. {codexspec-0.6.3 → codexspec-0.6.4}/src/codexspec/__init__.py +51 -5
  4. {codexspec-0.6.3 → codexspec-0.6.4}/src/codexspec/commands/installer.py +49 -1
  5. {codexspec-0.6.3 → codexspec-0.6.4}/src/codexspec/i18n.py +18 -0
  6. {codexspec-0.6.3 → codexspec-0.6.4}/src/codexspec/translator.py +34 -72
  7. {codexspec-0.6.3 → codexspec-0.6.4}/.gitignore +0 -0
  8. {codexspec-0.6.3 → codexspec-0.6.4}/LICENSE +0 -0
  9. {codexspec-0.6.3 → codexspec-0.6.4}/README.md +0 -0
  10. {codexspec-0.6.3 → codexspec-0.6.4}/codexspec-icon.svg +0 -0
  11. {codexspec-0.6.3 → codexspec-0.6.4}/codexspec-logo-dark.svg +0 -0
  12. {codexspec-0.6.3 → codexspec-0.6.4}/codexspec-logo-light.svg +0 -0
  13. {codexspec-0.6.3 → codexspec-0.6.4}/scripts/bash/check-i18n-completeness.sh +0 -0
  14. {codexspec-0.6.3 → codexspec-0.6.4}/scripts/bash/check-i18n-structure.sh +0 -0
  15. {codexspec-0.6.3 → codexspec-0.6.4}/scripts/bash/check-prerequisites.sh +0 -0
  16. {codexspec-0.6.3 → codexspec-0.6.4}/scripts/bash/common.sh +0 -0
  17. {codexspec-0.6.3 → codexspec-0.6.4}/scripts/bash/create-new-feature.sh +0 -0
  18. {codexspec-0.6.3 → codexspec-0.6.4}/scripts/powershell/check-prerequisites.ps1 +0 -0
  19. {codexspec-0.6.3 → codexspec-0.6.4}/scripts/powershell/common.ps1 +0 -0
  20. {codexspec-0.6.3 → codexspec-0.6.4}/scripts/powershell/create-new-feature.ps1 +0 -0
  21. {codexspec-0.6.3 → codexspec-0.6.4}/src/codexspec/commands/__init__.py +0 -0
  22. {codexspec-0.6.3 → codexspec-0.6.4}/src/codexspec/idea.md +0 -0
  23. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/analyze.md +0 -0
  24. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/checklist.md +0 -0
  25. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/clarify.md +0 -0
  26. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/commit-staged.md +0 -0
  27. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/config.md +0 -0
  28. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/constitution.md +0 -0
  29. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/generate-spec.md +0 -0
  30. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/implement-tasks.md +0 -0
  31. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/plan-to-tasks.md +0 -0
  32. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/pr.md +0 -0
  33. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/quick.md +0 -0
  34. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/review-code.md +0 -0
  35. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/review-plan.md +0 -0
  36. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/review-spec.md +0 -0
  37. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/review-tasks.md +0 -0
  38. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/spec-to-plan.md +0 -0
  39. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/specify.md +0 -0
  40. {codexspec-0.6.3 → codexspec-0.6.4}/templates/commands/tasks-to-issues.md +0 -0
  41. {codexspec-0.6.3 → codexspec-0.6.4}/templates/docs/checklist-template.md +0 -0
  42. {codexspec-0.6.3 → codexspec-0.6.4}/templates/docs/constitution-template.md +0 -0
  43. {codexspec-0.6.3 → codexspec-0.6.4}/templates/docs/plan-template-detailed.md +0 -0
  44. {codexspec-0.6.3 → codexspec-0.6.4}/templates/docs/plan-template-simple.md +0 -0
  45. {codexspec-0.6.3 → codexspec-0.6.4}/templates/docs/requirements-template.md +0 -0
  46. {codexspec-0.6.3 → codexspec-0.6.4}/templates/docs/spec-template-detailed.md +0 -0
  47. {codexspec-0.6.3 → codexspec-0.6.4}/templates/docs/spec-template-simple.md +0 -0
  48. {codexspec-0.6.3 → codexspec-0.6.4}/templates/docs/tasks-template-detailed.md +0 -0
  49. {codexspec-0.6.3 → codexspec-0.6.4}/templates/docs/tasks-template-simple.md +0 -0
  50. {codexspec-0.6.3 → codexspec-0.6.4}/templates/translations/de.json +0 -0
  51. {codexspec-0.6.3 → codexspec-0.6.4}/templates/translations/en.json +0 -0
  52. {codexspec-0.6.3 → codexspec-0.6.4}/templates/translations/es.json +0 -0
  53. {codexspec-0.6.3 → codexspec-0.6.4}/templates/translations/fr.json +0 -0
  54. {codexspec-0.6.3 → codexspec-0.6.4}/templates/translations/ja.json +0 -0
  55. {codexspec-0.6.3 → codexspec-0.6.4}/templates/translations/ko.json +0 -0
  56. {codexspec-0.6.3 → codexspec-0.6.4}/templates/translations/pt-BR.json +0 -0
  57. {codexspec-0.6.3 → codexspec-0.6.4}/templates/translations/zh-CN.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codexspec
3
- Version: 0.6.3
3
+ Version: 0.6.4
4
4
  Summary: CodexSpec - A Requirements-First SDD toolkit for Claude Code
5
5
  Project-URL: Homepage, https://github.com/Zts0hg/codexspec
6
6
  Project-URL: Repository, https://github.com/Zts0hg/codexspec
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codexspec"
3
- version = "0.6.3"
3
+ version = "0.6.4"
4
4
  description = "CodexSpec - A Requirements-First SDD toolkit for Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -26,9 +26,11 @@ from .commands.installer import (
26
26
  install_commands_to_subdir,
27
27
  migrate_old_commands,
28
28
  should_update_commands,
29
+ update_installed_command_frontmatter,
29
30
  )
30
31
  from .i18n import (
31
32
  generate_config_content,
33
+ get_explicit_language_key,
32
34
  get_interaction_language,
33
35
  get_language_name,
34
36
  get_supported_languages,
@@ -36,7 +38,7 @@ from .i18n import (
36
38
  normalize_locale,
37
39
  update_language_field,
38
40
  )
39
- from .translator import translate
41
+ from .translator import SUPPORTED_LANGUAGES, translate
40
42
 
41
43
  # Version info
42
44
  __version__ = "0.6.0"
@@ -270,6 +272,7 @@ def config(
270
272
  else:
271
273
  console.print("[red]Failed to update language setting[/red]")
272
274
  raise typer.Exit(1)
275
+ _rerender_command_frontmatter(config_file)
273
276
  return
274
277
 
275
278
  # Handle set commit language
@@ -348,6 +351,7 @@ def config(
348
351
  else:
349
352
  console.print("[red]Failed to update interaction language setting[/red]")
350
353
  raise typer.Exit(1)
354
+ _rerender_command_frontmatter(config_file)
351
355
  return
352
356
 
353
357
  # Handle set document language
@@ -553,21 +557,40 @@ def init(
553
557
  )
554
558
  prompted_base = False
555
559
  if base_determinable:
556
- normalized_lang = normalize_locale(output_value) if output_value else "en"
560
+ # Output base already resolved (or None when only dimension flags were
561
+ # given); no prompt needed to resolve it.
562
+ pass
557
563
  elif config_exists:
558
- normalized_lang = get_interaction_language(config_file)
564
+ # Existing project: preserve every unspecified key (REQ-007); no prompt.
565
+ pass
559
566
  elif sys.stdin.isatty():
560
567
  try:
561
568
  output_value = normalize_locale(prompt_language_selection())
562
569
  prompted_base = True
563
- normalized_lang = output_value
564
570
  except KeyboardInterrupt:
565
571
  console.print()
566
572
  console.print("[yellow]Selection cancelled, using default language (en)[/yellow]")
567
573
  output_value = "en"
568
- normalized_lang = "en"
569
574
  else:
570
575
  output_value = "en"
576
+
577
+ # Render/CLI-message language = the effective interaction language for THIS
578
+ # run (frontmatter descriptions are an interaction-language artifact). This is
579
+ # deliberately decoupled from output_value/prompted_base above, which only
580
+ # drive config writing and the first-time prompt. Precedence: explicit
581
+ # --interaction-lang -> explicitly-configured interaction -> --lang (output
582
+ # base) -> explicitly-configured output -> "en". Existing interaction is
583
+ # deliberately stronger than --lang; existing output is only a fallback when
584
+ # this run did not provide a new base.
585
+ if "interaction" in lang_overrides:
586
+ normalized_lang = lang_overrides["interaction"]
587
+ elif config_exists and (explicit_interaction := get_explicit_language_key(config_file, "interaction")) is not None:
588
+ normalized_lang = explicit_interaction
589
+ elif output_value is not None:
590
+ normalized_lang = output_value
591
+ elif config_exists and (explicit_output := get_explicit_language_key(config_file, "output")) is not None:
592
+ normalized_lang = explicit_output
593
+ else:
571
594
  normalized_lang = "en"
572
595
 
573
596
  if debug:
@@ -802,6 +825,29 @@ def init(
802
825
  console.print(f"[yellow]{translate('cli.init.important_action', normalized_lang)}[/yellow]")
803
826
 
804
827
 
828
+ def _rerender_command_frontmatter(config_file: Path) -> None:
829
+ """Re-render installed command frontmatter in the current interaction language.
830
+
831
+ Called after a ``config`` language change so ``.claude/commands/codexspec/*.md``
832
+ descriptions follow the new interaction language immediately, mirroring what
833
+ ``init`` does. No-op when commands are not installed. Languages without a
834
+ pre-translated cache are not re-rendered here (that path would invoke the
835
+ ``claude`` CLI once per command); the user is advised to run ``codexspec init``.
836
+ """
837
+ commands_subdir = Path.cwd() / ".claude" / "commands" / COMMANDS_SUBDIR
838
+ if not commands_subdir.exists():
839
+ return
840
+ language = get_interaction_language(config_file) # re-read AFTER the config write
841
+ if language != "en" and language not in SUPPORTED_LANGUAGES:
842
+ console.print(f"[dim]Run [cyan]codexspec init[/cyan] to update command descriptions for {language}.[/dim]")
843
+ return
844
+ templates_dir = get_templates_dir() / "commands"
845
+ if not templates_dir.exists():
846
+ return
847
+ update_installed_command_frontmatter(commands_subdir, templates_dir, language=language)
848
+ console.print(f"[dim]Updated command descriptions to: {get_language_name(language)}[/dim]")
849
+
850
+
805
851
  def _print_command_summary(language: str = "en") -> None:
806
852
  """Print a summary of installed commands grouped by category."""
807
853
  metadata = get_commands_metadata()
@@ -8,7 +8,12 @@ import shutil
8
8
  from pathlib import Path
9
9
  from typing import Optional, TypedDict
10
10
 
11
- from codexspec.translator import extract_frontmatter_fields, load_translation_cache, translate_template_frontmatter
11
+ from codexspec.translator import (
12
+ apply_translations_to_template,
13
+ extract_frontmatter_fields,
14
+ load_translation_cache,
15
+ translate_template_frontmatter,
16
+ )
12
17
 
13
18
  # Constants
14
19
  COMMANDS_SUBDIR = "codexspec" # Subdirectory name for commands
@@ -348,6 +353,49 @@ def install_commands_to_subdir(
348
353
  return installed_count
349
354
 
350
355
 
356
+ def update_installed_command_frontmatter(
357
+ target_dir: Path,
358
+ templates_dir: Path,
359
+ language: str = "en",
360
+ translations_dir: Optional[Path] = None,
361
+ ) -> int:
362
+ """Update frontmatter translations for already-installed command files.
363
+
364
+ This preserves command bodies and any non-translated frontmatter while
365
+ refreshing the language-specific ``description`` and ``argument-hint`` fields.
366
+ Missing installed commands are intentionally skipped.
367
+ """
368
+ if not target_dir.exists() or not templates_dir.exists():
369
+ return 0
370
+
371
+ translation_cache = None
372
+ if language != "en":
373
+ translation_cache = load_translation_cache(language, translations_dir)
374
+
375
+ updated_count = 0
376
+ for template_file in templates_dir.glob("*.md"):
377
+ target_path = target_dir / template_file.name
378
+ if not target_path.exists():
379
+ continue
380
+
381
+ template_content = template_file.read_text(encoding="utf-8")
382
+ template_name = template_file.stem
383
+ rendered_template = translate_template_frontmatter(template_content, template_name, language, translation_cache)
384
+ fields = {
385
+ key: value for key, value in extract_frontmatter_fields(rendered_template).items() if value is not None
386
+ }
387
+ if not fields:
388
+ continue
389
+
390
+ current_content = target_path.read_text(encoding="utf-8")
391
+ updated_content = apply_translations_to_template(current_content, fields)
392
+ if updated_content != current_content:
393
+ target_path.write_text(updated_content, encoding="utf-8")
394
+ updated_count += 1
395
+
396
+ return updated_count
397
+
398
+
351
399
  def should_update_commands(codexspec_dir: Path) -> bool:
352
400
  """Check if commands need updating (subdirectory already exists).
353
401
 
@@ -290,6 +290,24 @@ def _resolve_language(config_file: Path, primary_key: str) -> str:
290
290
  return "en"
291
291
 
292
292
 
293
+ def get_explicit_language_key(config_file: Optional[Path], key: str) -> Optional[str]:
294
+ """Return one explicitly configured ``language.<key>`` value, if present.
295
+
296
+ This differs from the public language-resolution helpers because it never
297
+ falls back to ``language.output`` or ``"en"``.
298
+ """
299
+ path = config_file or _default_config_path()
300
+ if not path.exists():
301
+ return None
302
+ try:
303
+ value = _read_language_key(path.read_text(encoding="utf-8"), key)
304
+ if value:
305
+ return normalize_locale(value) or None
306
+ except (OSError, re.error):
307
+ pass
308
+ return None
309
+
310
+
293
311
  def _default_config_path() -> Path:
294
312
  return Path.cwd() / ".codexspec" / "config.yml"
295
313
 
@@ -7,8 +7,6 @@ frontmatter fields (description, argument-hint).
7
7
  import json
8
8
  import logging
9
9
  import re
10
- import shutil
11
- import subprocess
12
10
  from pathlib import Path
13
11
  from typing import Optional
14
12
 
@@ -336,87 +334,56 @@ def apply_translations_to_template(content: str, translations: dict) -> str:
336
334
  Returns:
337
335
  Template content with translated frontmatter
338
336
  """
337
+ frontmatter_match = re.match(r"^(---\n)(.*?)(\n---(?:\n|$))", content, re.DOTALL)
338
+ if not frontmatter_match:
339
+ return content
340
+
341
+ prefix, frontmatter, suffix = frontmatter_match.groups()
342
+
339
343
  # Replace description
340
344
  if "description" in translations:
341
- content = re.sub(r"^(description:\s*).+$", rf"\g<1>{translations['description']}", content, flags=re.MULTILINE)
345
+ frontmatter = re.sub(
346
+ r"^(description:\s*).+$",
347
+ lambda match: f"{match.group(1)}{translations['description']}",
348
+ frontmatter,
349
+ flags=re.MULTILINE,
350
+ )
342
351
 
343
352
  # Replace argument-hint (handle both single-line and multiline)
344
353
  if "argument-hint" in translations:
345
354
  new_hint = translations["argument-hint"]
346
355
  # Check if original hint is multiline (uses |)
347
- if re.search(r"^argument-hint:\s*\|", content, re.MULTILINE):
356
+ if re.search(r"^argument-hint:\s*\|", frontmatter, re.MULTILINE):
348
357
  # Build new multiline content with proper indentation
349
358
  # Only add indentation to non-empty lines
350
359
  hint_lines = new_hint.split("\n")
351
360
  indented_lines = [(" " + line) if line else "" for line in hint_lines]
352
361
  indented_hint = "\n".join(indented_lines)
353
362
  # Replace multiline hint - match the entire multiline block (supports empty lines)
354
- content = re.sub(
363
+ frontmatter = re.sub(
355
364
  r"(argument-hint:\s*\|)\s*\n((?: .+\n?|\s*\n)+)",
356
- rf"\g<1>\n{indented_hint}\n",
357
- content,
365
+ lambda match: f"{match.group(1)}\n{indented_hint}\n",
366
+ frontmatter,
358
367
  flags=re.MULTILINE,
359
368
  )
360
369
  else:
361
370
  # Replace single-line hint (with or without quotes)
362
- if re.search(r'^argument-hint:\s*"', content, re.MULTILINE):
363
- content = re.sub(r'^(argument-hint:\s*)"[^"]+"$', rf'\g<1>"{new_hint}"', content, flags=re.MULTILINE)
371
+ if re.search(r'^argument-hint:\s*"', frontmatter, re.MULTILINE):
372
+ frontmatter = re.sub(
373
+ r'^(argument-hint:\s*)"[^"]+"$',
374
+ lambda match: f'{match.group(1)}"{new_hint}"',
375
+ frontmatter,
376
+ flags=re.MULTILINE,
377
+ )
364
378
  else:
365
- content = re.sub(r"^(argument-hint:\s*).+$", rf"\g<1>{new_hint}", content, flags=re.MULTILINE)
366
-
367
- return content
368
-
369
-
370
- def check_claude_cli_available() -> bool:
371
- """Check if Claude CLI is available.
372
-
373
- Returns:
374
- True if claude command is available
375
- """
376
- return shutil.which("claude") is not None
379
+ frontmatter = re.sub(
380
+ r"^(argument-hint:\s*).+$",
381
+ lambda match: f"{match.group(1)}{new_hint}",
382
+ frontmatter,
383
+ flags=re.MULTILINE,
384
+ )
377
385
 
378
-
379
- def translate_with_claude_cli(
380
- texts: dict[str, dict[str, str]], target_lang: str, timeout: int = 60
381
- ) -> Optional[dict[str, dict[str, str]]]:
382
- """Translate texts using Claude CLI.
383
-
384
- Args:
385
- texts: Dictionary of {command_name: {field: text}}
386
- target_lang: Target language code
387
- timeout: Timeout in seconds
388
-
389
- Returns:
390
- Translated dictionary or None on failure
391
- """
392
- if not check_claude_cli_available():
393
- return None
394
-
395
- # Build prompt
396
- input_json = json.dumps(texts, ensure_ascii=False, indent=2)
397
- prompt = f"""Translate the following JSON values to {target_lang}.
398
- Keep all keys unchanged. Return only valid JSON with the same structure.
399
- Do not translate technical terms like "spec.md", "plan.md", "TDD", etc.
400
-
401
- {input_json}"""
402
-
403
- try:
404
- result = subprocess.run(["claude", "--print", prompt], capture_output=True, text=True, timeout=timeout)
405
-
406
- if result.returncode != 0:
407
- return None
408
-
409
- # Parse JSON from output
410
- output = result.stdout.strip()
411
- # Remove potential markdown code blocks
412
- if output.startswith("```"):
413
- output = re.sub(r"^```(?:json)?\n?", "", output)
414
- output = re.sub(r"\n?```$", "", output)
415
-
416
- return json.loads(output)
417
-
418
- except (subprocess.TimeoutExpired, json.JSONDecodeError, OSError):
419
- return None
386
+ return f"{prefix}{frontmatter}{suffix}{content[frontmatter_match.end() :]}"
420
387
 
421
388
 
422
389
  def translate_template_frontmatter(
@@ -445,13 +412,8 @@ def translate_template_frontmatter(
445
412
  if cache and template_name in cache:
446
413
  return apply_translations_to_template(template_content, cache[template_name])
447
414
 
448
- # For non-cached languages, try dynamic translation
449
- if target_lang not in SUPPORTED_LANGUAGES:
450
- fields = extract_frontmatter_fields(template_content)
451
- if fields.get("description") or fields.get("argument-hint"):
452
- translations = translate_with_claude_cli({template_name: fields}, target_lang)
453
- if translations and template_name in translations:
454
- return apply_translations_to_template(template_content, translations[template_name])
455
-
456
- # Return original if all translation attempts fail
415
+ # No cache for this language: keep the original (English) frontmatter.
416
+ # Command templates are translated at runtime by Claude reading config.yml
417
+ # (see the template "Language Preference" section), never via a subprocess
418
+ # at install time.
457
419
  return template_content
File without changes
File without changes
File without changes
File without changes