mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
Integrate Junie with dot-to-hyphen behavior and command formatting (#4073)
* Add Junie integration with dot-to-hyphen behavior, command formatting, and file transformations. Based on Cline Integration. * Fix references to Cline in Junie integration and update class/test names for consistency. * Fix references to Cline in Junie integration and update class/test names for consistency. * Modified to generate correct formatting in junie
This commit is contained in:
@@ -4712,6 +4712,7 @@ class HookExecutor:
|
||||
kimi_skill_mode = selected_ai == "kimi"
|
||||
cline_mode = selected_ai == "cline"
|
||||
forge_mode = selected_ai == "forge"
|
||||
junie_mode = selected_ai == "junie"
|
||||
|
||||
skill_name = self._skill_name_from_command(command_id)
|
||||
if dollar_skill_mode and skill_name:
|
||||
@@ -4726,6 +4727,10 @@ class HookExecutor:
|
||||
from ..integrations.forge import format_forge_command_name
|
||||
|
||||
return f"/{format_forge_command_name(command_id)}"
|
||||
if junie_mode:
|
||||
from ..integrations.junie import format_junie_command_name
|
||||
|
||||
return f"/{format_junie_command_name(command_id)}"
|
||||
|
||||
use_slash = is_slash_skills_agent(selected_ai, ai_skills_enabled)
|
||||
|
||||
|
||||
@@ -1,6 +1,51 @@
|
||||
"""Junie integration (JetBrains)."""
|
||||
|
||||
from ..base import MarkdownIntegration
|
||||
from ..manifest import IntegrationManifest
|
||||
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# Note injected into hook sections so Junie maps dot-notation command
|
||||
# names (from extensions.yml) to the hyphenated slash commands it uses.
|
||||
_HOOK_COMMAND_NOTE = (
|
||||
"- When constructing slash commands from hook command names, "
|
||||
"replace dots (`.`) with hyphens (`-`). "
|
||||
"For example, `speckit.git.commit` → `/speckit-git-commit`.\n"
|
||||
)
|
||||
|
||||
|
||||
def format_junie_command_name(cmd_name: str) -> str:
|
||||
"""Convert command name to Junie-compatible hyphenated format.
|
||||
|
||||
Junie does not allow dots inside of slash-commands.
|
||||
This function converts dot-notation command names to hyphenated format.
|
||||
|
||||
The function is idempotent: already-formatted names are returned unchanged.
|
||||
|
||||
Examples:
|
||||
>>> format_junie_command_name("plan")
|
||||
'speckit-plan'
|
||||
>>> format_junie_command_name("speckit.plan")
|
||||
'speckit-plan'
|
||||
>>> format_junie_command_name("speckit.git.commit")
|
||||
'speckit-git-commit'
|
||||
|
||||
Args:
|
||||
cmd_name: Command name in dot notation (speckit.foo.bar),
|
||||
hyphenated format (speckit-foo-bar), or plain name (foo)
|
||||
|
||||
Returns:
|
||||
Hyphenated command name with 'speckit-' prefix
|
||||
"""
|
||||
cmd_name = cmd_name.replace(".", "-")
|
||||
|
||||
if not cmd_name.startswith("speckit-"):
|
||||
cmd_name = f"speckit-{cmd_name}"
|
||||
|
||||
return cmd_name
|
||||
|
||||
|
||||
class JunieIntegration(MarkdownIntegration):
|
||||
@@ -17,5 +62,117 @@ class JunieIntegration(MarkdownIntegration):
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
"inject_name": True,
|
||||
"format_name": format_junie_command_name,
|
||||
"invoke_separator": "-",
|
||||
}
|
||||
multi_install_safe = True
|
||||
invoke_separator = "-"
|
||||
|
||||
def command_filename(self, template_name: str) -> str:
|
||||
return format_junie_command_name(template_name) + ".md"
|
||||
|
||||
def build_command_invocation(self, command_name: str, args: str = "") -> str:
|
||||
"""Junie installs hyphenated slash-commands (``/speckit-<name>``), so the
|
||||
dispatch invocation must match. The inherited MarkdownIntegration default
|
||||
builds the dotted ``/speckit.<name>``, which references a command Junie
|
||||
never registered. Reuse the same hyphenation as command_filename /
|
||||
the injected frontmatter name (see ``format_junie_command_name``),
|
||||
mirroring the forge integration.
|
||||
"""
|
||||
invocation = "/" + format_junie_command_name(command_name)
|
||||
if args:
|
||||
invocation = f"{invocation} {args}"
|
||||
return invocation
|
||||
|
||||
def process_template(self, *args, **kwargs):
|
||||
"""Ensure shared templates render Junie command references with hyphens."""
|
||||
kwargs.setdefault("invoke_separator", self.invoke_separator)
|
||||
return super().process_template(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _inject_hook_command_note(content: str) -> str:
|
||||
"""Insert a dot-to-hyphen note before each hook output instruction.
|
||||
|
||||
Targets the line ``- For each executable hook, output the following``
|
||||
and inserts the note on the line before it, matching its indentation.
|
||||
Skips if the note is already present.
|
||||
"""
|
||||
if "replace dots" in content:
|
||||
return content
|
||||
|
||||
def repl(m: re.Match[str]) -> str:
|
||||
indent = m.group(1)
|
||||
instruction = m.group(2)
|
||||
# ``eol`` is empty when the regex matched via ``$`` because the
|
||||
# instruction was the final line of a file with no trailing
|
||||
# newline. Default to ``\n`` so the note never collapses onto
|
||||
# the same line as the instruction.
|
||||
eol = m.group(3) or "\n"
|
||||
return (
|
||||
indent
|
||||
+ _HOOK_COMMAND_NOTE.rstrip("\n")
|
||||
+ eol
|
||||
+ indent
|
||||
+ instruction
|
||||
+ eol
|
||||
)
|
||||
|
||||
return re.sub(
|
||||
r"(?m)^(\s*)(- For each executable hook, output the following[^\r\n]*)(\r\n|\n|$)",
|
||||
repl,
|
||||
content,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _rewrite_handoff_references(content: str) -> str:
|
||||
"""Replace dot-notation agent references in handoffs with hyphens."""
|
||||
return re.sub(
|
||||
r"(?m)^(\s*agent:\s*)(speckit\.[A-Za-z0-9-_]+(?:\.[A-Za-z0-9-_]+)*)",
|
||||
lambda m: f"{m.group(1)}{format_junie_command_name(m.group(2))}",
|
||||
content,
|
||||
)
|
||||
def post_process_command_content(self, content: str) -> str:
|
||||
"""Apply Junie-specific transformations to command content.
|
||||
|
||||
Overrides the ``IntegrationBase`` hook of the same name so that
|
||||
``CommandRegistrar.register_commands()`` (which dispatches to
|
||||
``post_process_command_content``) applies these transforms to
|
||||
extension/preset command files too, not just core commands.
|
||||
"""
|
||||
updated = self._inject_hook_command_note(content)
|
||||
updated = self._rewrite_handoff_references(updated)
|
||||
return updated
|
||||
|
||||
def setup(
|
||||
self,
|
||||
project_root: Path,
|
||||
manifest: IntegrationManifest,
|
||||
parsed_options: dict[str, Any] | None = None,
|
||||
**opts: Any,
|
||||
) -> list[Path]:
|
||||
"""Install Junie commands and apply post-processing transformations."""
|
||||
created = super().setup(project_root, manifest, parsed_options, **opts)
|
||||
|
||||
# Post-process generated command files
|
||||
dest_dir = self.commands_dest(project_root).resolve()
|
||||
|
||||
for path in created:
|
||||
# Only touch .md files under the commands directory
|
||||
try:
|
||||
path.resolve().relative_to(dest_dir)
|
||||
except ValueError:
|
||||
continue
|
||||
if path.suffix != ".md":
|
||||
continue
|
||||
|
||||
content_bytes = path.read_bytes()
|
||||
content = content_bytes.decode("utf-8")
|
||||
|
||||
updated = self.post_process_command_content(content)
|
||||
|
||||
if updated != content:
|
||||
path.write_bytes(updated.encode("utf-8"))
|
||||
self.record_file_in_manifest(path, project_root, manifest)
|
||||
|
||||
return created
|
||||
|
||||
Reference in New Issue
Block a user