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.
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/CHANGELOG.md +25 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/PKG-INFO +2 -2
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/README.md +1 -1
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/pyproject.toml +1 -1
- datasecops_cli-0.6.0/src/datasecops_cli/__init__.py +1 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/menus/development.py +9 -5
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/menus/downloads.py +1 -1
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/menus/git_operations.py +40 -1
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/git_service.py +26 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/linting_service.py +15 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/skill_service.py +67 -23
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/utilities/file_utils.py +4 -0
- datasecops_cli-0.5.9/src/datasecops_cli/__init__.py +0 -1
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/.github/workflows/auto-tag.yml +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/.github/workflows/publish-cli.yml +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/.github/workflows/test.yml +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/.gitignore +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/DEVELOPMENT.md +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/LICENSE +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/docs/getting-started.md +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/docs/git-operations.md +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/docs/legacy.md +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/docs/legacy_plan_of_action.md +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/docs/mcp-server.md +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/mcp-servers.json +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/setup.ps1 +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/setup.sh +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/config.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/main.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/menus/__init__.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/menus/configuration.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/models/__init__.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/models/git_helpers.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/models/project_config.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/__init__.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/bootstrap_service.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/dbt_project_generator.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/dbt_runner.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/download_service.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/snowflake_service.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/upstream_service.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/utilities/__init__.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/utilities/display.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/utilities/mcp.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_mcp/__init__.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_mcp/__main__.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_mcp/connection.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_mcp/server.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/__init__.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_config.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_file_utils.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_main.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_models.py +0 -0
- {datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/tests/test_version.py +0 -0
- {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.
|
|
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)
|
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
218
|
-
if
|
|
219
|
-
self.linting.
|
|
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 -
|
|
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}")
|
{datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/linting_service.py
RENAMED
|
@@ -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 = ["
|
|
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.
|
|
72
|
-
if "
|
|
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("
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
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
|
|
150
|
-
"""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
content
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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."""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.3"
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/bootstrap_service.py
RENAMED
|
File without changes
|
{datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/dbt_project_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/directory_scaffolder.py
RENAMED
|
File without changes
|
{datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/download_service.py
RENAMED
|
File without changes
|
{datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/snowflake_service.py
RENAMED
|
File without changes
|
{datasecops_cli-0.5.9 → datasecops_cli-0.6.0}/src/datasecops_cli/services/upstream_service.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|