datasecops-cli 0.5.9__tar.gz → 0.6.0__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. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/CHANGELOG.md +25 -0
  2. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/PKG-INFO +2 -2
  3. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/README.md +1 -1
  4. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/pyproject.toml +1 -1
  5. datasecops_cli-0.6.0/src/datasecops_cli/__init__.py +1 -0
  6. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/menus/development.py +9 -5
  7. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/menus/downloads.py +1 -1
  8. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/menus/git_operations.py +40 -1
  9. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/git_service.py +26 -0
  10. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/linting_service.py +15 -0
  11. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/skill_service.py +67 -23
  12. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/utilities/file_utils.py +4 -0
  13. datasecops_cli-0.5.9/src/datasecops_cli/__init__.py +0 -1
  14. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/.github/workflows/auto-tag.yml +0 -0
  15. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/.github/workflows/publish-cli.yml +0 -0
  16. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/.github/workflows/test.yml +0 -0
  17. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/.gitignore +0 -0
  18. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/DEVELOPMENT.md +0 -0
  19. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/LICENSE +0 -0
  20. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/docs/getting-started.md +0 -0
  21. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/docs/git-operations.md +0 -0
  22. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/docs/legacy.md +0 -0
  23. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/docs/legacy_plan_of_action.md +0 -0
  24. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/docs/mcp-server.md +0 -0
  25. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/mcp-servers.json +0 -0
  26. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/setup.ps1 +0 -0
  27. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/setup.sh +0 -0
  28. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/config.py +0 -0
  29. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/main.py +0 -0
  30. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/menus/__init__.py +0 -0
  31. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/menus/configuration.py +0 -0
  32. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/models/__init__.py +0 -0
  33. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/models/git_helpers.py +0 -0
  34. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/models/project_config.py +0 -0
  35. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/__init__.py +0 -0
  36. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/bootstrap_service.py +0 -0
  37. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/dbt_project_generator.py +0 -0
  38. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/dbt_runner.py +0 -0
  39. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
  40. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/download_service.py +0 -0
  41. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/snowflake_service.py +0 -0
  42. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/upstream_service.py +0 -0
  43. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/utilities/__init__.py +0 -0
  44. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/utilities/display.py +0 -0
  45. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/utilities/mcp.py +0 -0
  46. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
  47. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_mcp/__init__.py +0 -0
  48. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_mcp/__main__.py +0 -0
  49. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_mcp/connection.py +0 -0
  50. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_mcp/server.py +0 -0
  51. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/__init__.py +0 -0
  52. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_config.py +0 -0
  53. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_file_utils.py +0 -0
  54. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_main.py +0 -0
  55. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_models.py +0 -0
  56. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_version.py +0 -0
  57. {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_yaml_utils.py +0 -0
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to the DataSecOps CLI are documented in this file.
4
4
 
5
+ ## [0.6.0] - 2026-07-24
6
+
7
+ ### Added
8
+
9
+ - **GitHub Copilot skill target** — skills can now be installed as `.github/instructions/<id>.instructions.md` files with `applyTo` frontmatter for GitHub Copilot custom instructions.
10
+ - **Multi-tool skill download** — the skill install menu now prompts users to select target tools: CoCo, Cursor, Copilot, and/or Claude Code. Each format is automatically generated from the same source skill.
11
+ - **Snowflake query instructions** — Cursor, Copilot, and Claude skill files automatically include instructions on how to query Snowflake via the `snow` CLI when the skill contains SQL blocks.
12
+
13
+ ### Changed
14
+
15
+ - **Skill target renamed** — "Cortex Code" target label renamed to "CoCo" to match the native app UI naming convention.
16
+ - **Downloads menu label** — "Cortex Code skills" renamed to "AI coding skills".
17
+ - **Shared conversion helpers** — extracted `_strip_frontmatter()` and `_prepare_body()` to eliminate duplication across format converters.
18
+ - **Robust frontmatter stripping** — uses line-boundary matching (`\n---`) instead of simple `split('---', 2)` to avoid false matches in skill body content.
19
+ - **Cursor description quoting** — `.mdc` description values are now properly quoted and sanitized (newlines removed, quotes escaped).
20
+
21
+ ## [0.5.10] - 2026-06-26
22
+
23
+ ### Changed
24
+
25
+ - **Lint modified includes all uncommitted files** — "lint modified" and "fix modified" now detect staged changes, unstaged changes, and untracked files (previously only caught unstaged modifications).
26
+ - **Lint specific file accepts model names** — the "specific" lint option now accepts a model name (e.g. `stg_orders`) and resolves it to the SQL file path by searching the models directory recursively.
27
+ - **Reset branch from main** — new git menu option (10) to force-reset an environment branch (e.g. `test`) to match `origin/main`, removing all existing commits on that branch.
28
+ - **Deploy excludes main branch** — the deploy-to-environment menu no longer lists `main` as a target, preventing accidental force-pushes to the primary branch.
29
+
5
30
  ## [0.5.9] - 2026-06-22
6
31
 
7
32
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasecops-cli
3
- Version: 0.5.9
3
+ Version: 0.6.0
4
4
  Summary: DataSecOps Framework CLI for Snowflake Native App
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -81,7 +81,7 @@ datasecops
81
81
  | Menu | Capabilities |
82
82
  |------|-------------|
83
83
  | **Development** | dbt run, build, test, lint (SQLFluff), deps, seed, compile, snapshot, freshness, docs |
84
- | **Git** | Branch create/checkout/delete, commit & push, rebase, squash, deploy to environment branches, cherry-pick |
84
+ | **Git** | Branch create/checkout/delete, commit & push, rebase, squash, deploy to environment branches, cherry-pick, reset branch from main |
85
85
  | **Downloads** | SQLFluff config, CI/CD pipelines (GitHub Actions / Azure DevOps), dbt packages, Cortex Code skills |
86
86
 
87
87
  ## Non-Interactive Mode (CI/CD)
@@ -62,7 +62,7 @@ datasecops
62
62
  | Menu | Capabilities |
63
63
  |------|-------------|
64
64
  | **Development** | dbt run, build, test, lint (SQLFluff), deps, seed, compile, snapshot, freshness, docs |
65
- | **Git** | Branch create/checkout/delete, commit & push, rebase, squash, deploy to environment branches, cherry-pick |
65
+ | **Git** | Branch create/checkout/delete, commit & push, rebase, squash, deploy to environment branches, cherry-pick, reset branch from main |
66
66
  | **Downloads** | SQLFluff config, CI/CD pipelines (GitHub Actions / Azure DevOps), dbt packages, Cortex Code skills |
67
67
 
68
68
  ## Non-Interactive Mode (CI/CD)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datasecops-cli"
7
- version = "0.5.9"
7
+ version = "0.6.0"
8
8
  description = "DataSecOps Framework CLI for Snowflake Native App"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ __version__ = "0.6.0"
@@ -204,19 +204,23 @@ class DevelopmentMenu:
204
204
  complete_action()
205
205
  return
206
206
  if option == 1:
207
- changed = [f.file for f in self.git.get_changed_files()] if self.git else []
207
+ changed = self.git.get_all_modified_files() if self.git else []
208
208
  self.linting.lint_modified(fix=False, changed_files=changed)
209
209
  elif option == 2:
210
- changed = [f.file for f in self.git.get_changed_files()] if self.git else []
210
+ changed = self.git.get_all_modified_files() if self.git else []
211
211
  self.linting.lint_modified(fix=True, changed_files=changed)
212
212
  elif option == 3:
213
213
  self.linting.lint_file(fix=False)
214
214
  elif option == 4:
215
215
  self.linting.lint_file(fix=True)
216
216
  elif option == 5:
217
- path = get_input_string("Enter SQL file path (e.g. models/staging/stg_orders.sql): ")
218
- if path != "0":
219
- self.linting.lint_file(file_path=path, fix=False)
217
+ name = get_input_string("Enter model name or file path (e.g. stg_orders): ")
218
+ if name != "0":
219
+ path = name if name.endswith(".sql") else self.linting.resolve_model_path(name)
220
+ if path:
221
+ self.linting.lint_file(file_path=path, fix=False)
222
+ else:
223
+ error_line(f"Could not find model '{name}' in models/ directory")
220
224
  elif option == 6:
221
225
  if not self.downloads:
222
226
  error_line("Download service not available")
@@ -70,7 +70,7 @@ class DownloadsMenu:
70
70
  menu_option(2, "pipelines - Download CI/CD pipeline files")
71
71
  menu_option(3, "dbt packages - Download dbt package versions")
72
72
  menu_option(4, "macros - Download default macros for this profile")
73
- menu_option(5, "skills - Cortex Code skills")
73
+ menu_option(5, "skills - AI coding skills")
74
74
  menu_option(0, "back - Return to main menu")
75
75
 
76
76
  def _skills_menu(self) -> None:
@@ -42,6 +42,8 @@ class GitOperationsMenu:
42
42
  self._cherry_pick_test()
43
43
  elif option == 9:
44
44
  self._deploy_prod()
45
+ elif option == 10:
46
+ self._reset_branch_from_main()
45
47
  self._menu()
46
48
  option = get_input_number("Choose an option: ")
47
49
 
@@ -57,6 +59,7 @@ class GitOperationsMenu:
57
59
  menu_option(7, "squash to test - Squash merge into test")
58
60
  menu_option(8, "remove from test - Remove a branch from test")
59
61
  menu_option(9, "deploy to prod - Deploy main to production")
62
+ menu_option(10, "reset branch - Force reset a branch from main")
60
63
  menu_option(0, "back - Return to main menu")
61
64
 
62
65
  def _branch_menu(self) -> None:
@@ -190,7 +193,7 @@ class GitOperationsMenu:
190
193
  display_action_header("Deploy to Environment")
191
194
 
192
195
  # Build environment list from framework config, filtering non-deployable environments
193
- excluded = {"unit-test", "ci", "local-dev", "prod", "test"}
196
+ excluded = {"unit-test", "ci", "local-dev", "prod", "test", "main"}
194
197
  environments = {}
195
198
  if self.source_control.environments:
196
199
  for env in self.source_control.environments:
@@ -289,3 +292,39 @@ class GitOperationsMenu:
289
292
  selected = commits[choice-1]
290
293
  self.git.revert_from_test(selected["sha"], selected["branch"])
291
294
  complete_action()
295
+
296
+ def _reset_branch_from_main(self) -> None:
297
+ display_action_header("Reset Branch from Main")
298
+
299
+ # Build list of resettable branches from environments + deployment_branches
300
+ excluded = {"main", "prod"}
301
+ branches = {}
302
+ if self.source_control.environments:
303
+ for env in self.source_control.environments:
304
+ if env.branch_name and env.branch_name.lower() not in excluded:
305
+ branches[env.branch_name] = env.branch_name
306
+
307
+ if not branches:
308
+ branches = {
309
+ k: v for k, v in self.deployment_branches.items()
310
+ if k.lower() not in excluded
311
+ }
312
+
313
+ # Always include test if not already present
314
+ if "test" not in branches:
315
+ branches["test"] = "test"
316
+
317
+ if not branches:
318
+ error_line("No branches available to reset")
319
+ complete_action()
320
+ return
321
+
322
+ selected = select_from_dict(branches, "branch to reset")
323
+ if selected == "back":
324
+ return
325
+
326
+ warning_line(f"This will FORCE RESET '{selected}' to match origin/main")
327
+ warning_line("All existing commits on that branch will be lost")
328
+ if get_input_true_false("Are you sure?", "n"):
329
+ self.git.reset_branch_from_main(selected)
330
+ complete_action()
@@ -30,6 +30,22 @@ class GitService:
30
30
  def get_new_files(self) -> list[GitCommitHelper]:
31
31
  return [GitCommitHelper(file=f) for f in self.repo.untracked_files]
32
32
 
33
+ def get_all_modified_files(self) -> list[str]:
34
+ """Get all files that differ from the last commit: unstaged, staged, and untracked."""
35
+ paths = set()
36
+ # Staged changes (index vs HEAD)
37
+ for d in self.repo.index.diff("HEAD"):
38
+ if d.change_type != "D" and d.a_path:
39
+ paths.add(d.a_path)
40
+ # Unstaged changes (working tree vs index)
41
+ for d in self.repo.index.diff(None):
42
+ if d.change_type != "D" and d.a_path:
43
+ paths.add(d.a_path)
44
+ # Untracked files
45
+ for f in self.repo.untracked_files:
46
+ paths.add(f)
47
+ return sorted(paths)
48
+
33
49
  def get_local_branches(self) -> dict[str, str]:
34
50
  return {b.name: b.name for b in self.repo.branches}
35
51
 
@@ -397,3 +413,13 @@ class GitService:
397
413
  if branch.name not in ("main", "test"):
398
414
  self.repo.delete_head(branch, force=True)
399
415
  success_line("Reset to main completed")
416
+
417
+ def reset_branch_from_main(self, branch_name: str) -> None:
418
+ """Force reset a remote branch to match origin/main."""
419
+ try:
420
+ self.repo.remotes.origin.fetch()
421
+ main_sha = self.repo.commit("origin/main").hexsha
422
+ self.repo.remotes.origin.push(refspec=f"{main_sha}:refs/heads/{branch_name}", force=True)
423
+ success_line(f"Reset '{branch_name}' to match main ({main_sha[:8]})")
424
+ except GitCommandError as e:
425
+ error_line(f"Failed to reset '{branch_name}': {e}")
@@ -88,6 +88,21 @@ class LintingService:
88
88
  warning_line(f"Missing packages: {', '.join(missing)}")
89
89
  return self.install_requirements(missing)
90
90
 
91
+ def resolve_model_path(self, name: str) -> str | None:
92
+ """Resolve a model name (e.g. 'stg_orders') to its SQL file path under models/."""
93
+ models_dir = self.project_dir / "models"
94
+ if not models_dir.exists():
95
+ return None
96
+ matches = list(models_dir.rglob(f"{name}.sql"))
97
+ if len(matches) == 1:
98
+ return str(matches[0].relative_to(self.project_dir))
99
+ if len(matches) > 1:
100
+ warning_line(f"Multiple matches for '{name}':")
101
+ for m in matches:
102
+ info_line(f" {m.relative_to(self.project_dir)}")
103
+ return None
104
+ return None
105
+
91
106
  def lint_file(self, file_path: str = None, fix: bool = False) -> subprocess.CompletedProcess:
92
107
  action = "fix" if fix else "lint"
93
108
  cmd = ["sqlfluff", action]
@@ -7,14 +7,26 @@ from datasecops_cli.models.project_config import CortexSkill, CortexSkillFile
7
7
  from datasecops_cli.utilities.display import info_line, success_line, error_line, warning_line
8
8
  from datasecops_cli.utilities.file_utils import (
9
9
  write_file, ensure_dir, get_cortex_skills_dir,
10
- get_cursor_rules_dir, get_claude_commands_dir,
10
+ get_cursor_rules_dir, get_claude_commands_dir, get_copilot_instructions_dir,
11
11
  )
12
12
 
13
+ _SNOWFLAKE_QUERY_INSTRUCTIONS = """## Querying Snowflake
14
+
15
+ This skill requires running SQL queries against Snowflake to retrieve configuration.
16
+ Use the Snowflake CLI (`snow`) to execute queries:
17
+
18
+ ```bash
19
+ snow sql -q "YOUR SQL QUERY HERE" -c <connection_name>
20
+ ```
21
+
22
+ Alternatively, if an MCP server for Snowflake is configured, use it to execute queries directly.
23
+ """
24
+
13
25
 
14
26
  class SkillService:
15
27
  """Manages skill installation for Cortex Code, Cursor, and Claude Code."""
16
28
 
17
- TARGETS = ["Cortex Code", "Cursor", "Claude Code"]
29
+ TARGETS = ["CoCo", "Cursor", "Copilot", "Claude Code"]
18
30
 
19
31
  def __init__(self, snowflake_service: SnowflakeService):
20
32
  self.sf = snowflake_service
@@ -68,15 +80,15 @@ class SkillService:
68
80
  targets = self.TARGETS
69
81
  targets_installed: list[str] = []
70
82
 
71
- # 1. Cortex Code: .cortex/skills/{skill_id}/SKILL.md + extra files
72
- if "Cortex Code" in targets:
83
+ # 1. CoCo: .cortex/skills/{skill_id}/SKILL.md + extra files
84
+ if "CoCo" in targets:
73
85
  cortex_dir = target_dir or get_cortex_skills_dir()
74
86
  skill_dir = cortex_dir / skill.skill_id
75
87
  ensure_dir(skill_dir)
76
88
  for f in skill.files:
77
89
  if f.filename and f.content:
78
90
  write_file(skill_dir / f.filename, f.content)
79
- targets_installed.append("Cortex Code")
91
+ targets_installed.append("CoCo")
80
92
 
81
93
  # Extract SKILL.md content for Cursor and Claude Code
82
94
  skill_md = self._get_skill_md_content(skill)
@@ -97,11 +109,23 @@ class SkillService:
97
109
  try:
98
110
  claude_dir = get_claude_commands_dir()
99
111
  ensure_dir(claude_dir)
100
- write_file(claude_dir / f"{skill.skill_id}.md", skill_md)
112
+ claude_content = self._to_claude_command(skill, skill_md)
113
+ write_file(claude_dir / f"{skill.skill_id}.md", claude_content)
101
114
  targets_installed.append("Claude Code")
102
115
  except OSError as e:
103
116
  warning_line(f" Claude Code install failed: {e}")
104
117
 
118
+ # 4. GitHub Copilot: .github/instructions/{skill_id}.instructions.md
119
+ if "Copilot" in targets and skill_md:
120
+ try:
121
+ copilot_dir = get_copilot_instructions_dir()
122
+ ensure_dir(copilot_dir)
123
+ copilot_content = self._to_copilot_instructions(skill, skill_md)
124
+ write_file(copilot_dir / f"{skill.skill_id}.instructions.md", copilot_content)
125
+ targets_installed.append("Copilot")
126
+ except OSError as e:
127
+ warning_line(f" Copilot install failed: {e}")
128
+
105
129
  success_line(f"Installed skill: {skill.name} v{skill.version} -> {', '.join(targets_installed)}")
106
130
  return True
107
131
 
@@ -113,7 +137,7 @@ class SkillService:
113
137
  for skill in skills:
114
138
  if self.install_skill(skill, targets=targets, target_dir=target_dir):
115
139
  count += 1
116
- if "Cortex Code" in targets:
140
+ if "CoCo" in targets:
117
141
  self._register_cortex_skills_dir(target_dir)
118
142
  success_line(f"Installed {count} skill(s) to {', '.join(targets)}")
119
143
  return count
@@ -128,7 +152,7 @@ class SkillService:
128
152
  if self._is_installed(skill.skill_id, target):
129
153
  self.install_skill(skill, targets=targets, target_dir=target_dir)
130
154
  count += 1
131
- if "Cortex Code" in targets:
155
+ if "CoCo" in targets:
132
156
  self._register_cortex_skills_dir(target_dir)
133
157
  success_line(f"Updated {count} skill(s)")
134
158
  return count
@@ -146,23 +170,32 @@ class SkillService:
146
170
  return ""
147
171
 
148
172
  @staticmethod
149
- def _to_cursor_rule(skill: CortexSkill, skill_md_content: str) -> str:
150
- """Convert SKILL.md content to Cursor .mdc rule format.
151
-
152
- Cursor rules use the same markdown format but with optional
153
- frontmatter for description and globs.
154
- """
155
- # Strip the existing YAML frontmatter if present
156
- content = skill_md_content
157
- if content.startswith("---"):
158
- end = content.find("---", 3)
159
- if end != -1:
160
- content = content[end + 3:].lstrip("\n")
161
-
162
- # Add Cursor-style frontmatter
173
+ def _strip_frontmatter(content: str) -> str:
174
+ """Remove leading YAML frontmatter (---...---) from markdown content."""
175
+ if not content.startswith("---"):
176
+ return content
177
+ end = content.find("\n---", 3)
178
+ if end == -1:
179
+ return content
180
+ return content[end + 4:].lstrip("\n")
181
+
182
+ @classmethod
183
+ def _prepare_body(cls, skill_md_content: str) -> str:
184
+ """Strip frontmatter and prepend Snowflake query instructions if needed."""
185
+ body = cls._strip_frontmatter(skill_md_content)
186
+ if "```sql" in body:
187
+ body = _SNOWFLAKE_QUERY_INSTRUCTIONS + "\n" + body
188
+ return body
189
+
190
+ @classmethod
191
+ def _to_cursor_rule(cls, skill: CortexSkill, skill_md_content: str) -> str:
192
+ """Convert SKILL.md content to Cursor .mdc rule format."""
193
+ content = cls._prepare_body(skill_md_content)
194
+ # Sanitize description for single-line YAML value
195
+ description = (skill.description or "").replace("\n", " ").replace('"', '\\"')
163
196
  lines = [
164
197
  "---",
165
- f"description: {skill.description}",
198
+ f'description: "{description}"',
166
199
  "globs: []",
167
200
  "alwaysApply: false",
168
201
  "---",
@@ -171,6 +204,17 @@ class SkillService:
171
204
  ]
172
205
  return "\n".join(lines)
173
206
 
207
+ @classmethod
208
+ def _to_claude_command(cls, skill: CortexSkill, skill_md_content: str) -> str:
209
+ """Convert SKILL.md content to Claude Code command format."""
210
+ return cls._prepare_body(skill_md_content)
211
+
212
+ @classmethod
213
+ def _to_copilot_instructions(cls, skill: CortexSkill, skill_md_content: str) -> str:
214
+ """Convert SKILL.md content to GitHub Copilot instructions format."""
215
+ content = cls._prepare_body(skill_md_content)
216
+ return f'---\napplyTo: "**"\n---\n\n{content}'
217
+
174
218
  @staticmethod
175
219
  def _register_cortex_skills_dir(target_dir: Path = None) -> None:
176
220
  """Register the skills directory with Cortex Code CLI so skills are discoverable."""
@@ -39,3 +39,7 @@ def get_cursor_rules_dir() -> Path:
39
39
 
40
40
  def get_claude_commands_dir() -> Path:
41
41
  return Path.cwd() / ".claude" / "commands"
42
+
43
+
44
+ def get_copilot_instructions_dir() -> Path:
45
+ return Path.cwd() / ".github" / "instructions"
@@ -1 +0,0 @@
1
- __version__ = "0.5.3"
File without changes
File without changes
File without changes