structkit 3.0.0__py3-none-any.whl
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.
- structkit/__init__.py +6 -0
- structkit/commands/__init__.py +17 -0
- structkit/commands/completion.py +65 -0
- structkit/commands/generate.py +397 -0
- structkit/commands/generate_schema.py +67 -0
- structkit/commands/import.py +63 -0
- structkit/commands/info.py +87 -0
- structkit/commands/init.py +52 -0
- structkit/commands/list.py +89 -0
- structkit/commands/mcp.py +100 -0
- structkit/commands/validate.py +129 -0
- structkit/completers.py +54 -0
- structkit/content_fetcher.py +249 -0
- structkit/contribs/README.md +271 -0
- structkit/contribs/ansible-playbook.yaml +38 -0
- structkit/contribs/chef-cookbook.yaml +51 -0
- structkit/contribs/ci-cd-pipelines.yaml +67 -0
- structkit/contribs/cloudformation-files.yaml +21 -0
- structkit/contribs/configs/chglog.yaml +31 -0
- structkit/contribs/configs/codeowners.yaml +3 -0
- structkit/contribs/configs/devcontainer.yaml +35 -0
- structkit/contribs/configs/editor-config.yaml +11 -0
- structkit/contribs/configs/eslint.yaml +30 -0
- structkit/contribs/configs/jshint.yaml +11 -0
- structkit/contribs/configs/kubectl.yaml +23 -0
- structkit/contribs/configs/prettier.yaml +19 -0
- structkit/contribs/docker-files.yaml +27 -0
- structkit/contribs/documentation-template.yaml +33 -0
- structkit/contribs/git-hooks.yaml +19 -0
- structkit/contribs/github/chatmodes/plan.yaml +18 -0
- structkit/contribs/github/instructions/generic.yaml +5 -0
- structkit/contribs/github/prompts/generic.yaml +4 -0
- structkit/contribs/github/prompts/react-form.yaml +17 -0
- structkit/contribs/github/prompts/security-api.yaml +8 -0
- structkit/contribs/github/prompts/struct.yaml +90 -0
- structkit/contribs/github/templates.yaml +91 -0
- structkit/contribs/github/workflows/codeql.yaml +88 -0
- structkit/contribs/github/workflows/execute-tf-workflow.yaml +39 -0
- structkit/contribs/github/workflows/labeler.yaml +77 -0
- structkit/contribs/github/workflows/pre-commit.yaml +27 -0
- structkit/contribs/github/workflows/release-drafter.yaml +77 -0
- structkit/contribs/github/workflows/run-struct.yaml +30 -0
- structkit/contribs/github/workflows/stale.yaml +16 -0
- structkit/contribs/helm-chart.yaml +160 -0
- structkit/contribs/kubernetes-manifests.yaml +103 -0
- structkit/contribs/project/custom-structures.yaml +24 -0
- structkit/contribs/project/generic.yaml +309 -0
- structkit/contribs/project/go.yaml +104 -0
- structkit/contribs/project/java.yaml +85 -0
- structkit/contribs/project/n8n.yaml +100 -0
- structkit/contribs/project/nodejs.yaml +101 -0
- structkit/contribs/project/python.yaml +136 -0
- structkit/contribs/project/ruby.yaml +130 -0
- structkit/contribs/project/rust.yaml +106 -0
- structkit/contribs/prompts/run-struct-trigger.yaml +18 -0
- structkit/contribs/terraform/apps/aws-accounts.yaml +21 -0
- structkit/contribs/terraform/apps/environments.yaml +41 -0
- structkit/contribs/terraform/apps/generic.yaml +41 -0
- structkit/contribs/terraform/apps/github-organization.yaml +40 -0
- structkit/contribs/terraform/apps/init.yaml +11 -0
- structkit/contribs/terraform/modules/generic.yaml +58 -0
- structkit/contribs/vagrant-files.yaml +21 -0
- structkit/file_item.py +182 -0
- structkit/filters.py +112 -0
- structkit/input_store.py +35 -0
- structkit/logging_config.py +36 -0
- structkit/main.py +85 -0
- structkit/mcp_server.py +347 -0
- structkit/model_wrapper.py +47 -0
- structkit/template_renderer.py +258 -0
- structkit/utils.py +36 -0
- structkit-3.0.0.dist-info/METADATA +182 -0
- structkit-3.0.0.dist-info/RECORD +77 -0
- structkit-3.0.0.dist-info/WHEEL +5 -0
- structkit-3.0.0.dist-info/entry_points.txt +2 -0
- structkit-3.0.0.dist-info/licenses/LICENSE +201 -0
- structkit-3.0.0.dist-info/top_level.txt +1 -0
structkit/file_item.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# FILE: file_item.py
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
from structkit.template_renderer import TemplateRenderer
|
|
9
|
+
from structkit.content_fetcher import ContentFetcher
|
|
10
|
+
from structkit.model_wrapper import ModelWrapper
|
|
11
|
+
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
class FileItem:
|
|
15
|
+
def __init__(self, properties):
|
|
16
|
+
self.logger = logging.getLogger(__name__)
|
|
17
|
+
self.name = properties.get("name")
|
|
18
|
+
self.file_directory = self._get_file_directory()
|
|
19
|
+
self.content = properties.get("content")
|
|
20
|
+
self.config_variables = properties.get("config_variables")
|
|
21
|
+
self.content_location = properties.get("file")
|
|
22
|
+
self.permissions = properties.get("permissions")
|
|
23
|
+
self.input_store = properties.get("input_store")
|
|
24
|
+
self.non_interactive = properties.get("non_interactive")
|
|
25
|
+
self.skip = properties.get("skip", False)
|
|
26
|
+
self.skip_if_exists = properties.get("skip_if_exists", False)
|
|
27
|
+
|
|
28
|
+
self.content_fetcher = ContentFetcher()
|
|
29
|
+
|
|
30
|
+
self.system_prompt = properties.get("system_prompt") or properties.get("global_system_prompt")
|
|
31
|
+
self.user_prompt = properties.get("user_prompt")
|
|
32
|
+
self.mappings = properties.get("mappings", {})
|
|
33
|
+
|
|
34
|
+
self.model_wrapper = ModelWrapper(self.logger)
|
|
35
|
+
|
|
36
|
+
self.template_renderer = TemplateRenderer(
|
|
37
|
+
self.config_variables,
|
|
38
|
+
self.input_store,
|
|
39
|
+
self.non_interactive,
|
|
40
|
+
self.mappings
|
|
41
|
+
)
|
|
42
|
+
# internal flags used for reporting
|
|
43
|
+
self._last_action = None
|
|
44
|
+
|
|
45
|
+
def _get_file_directory(self):
|
|
46
|
+
return os.path.dirname(self.name)
|
|
47
|
+
|
|
48
|
+
def process_prompt(self, dry_run=False, existing_content=None):
|
|
49
|
+
if self.user_prompt:
|
|
50
|
+
|
|
51
|
+
if not self.system_prompt:
|
|
52
|
+
system_prompt = "You are a software developer working on a project. You need to create a file with the following content:"
|
|
53
|
+
else:
|
|
54
|
+
system_prompt = self.system_prompt
|
|
55
|
+
|
|
56
|
+
user_prompt = self.user_prompt
|
|
57
|
+
if existing_content:
|
|
58
|
+
user_prompt += f"\n\nCurrent file content (if any):\n```\n{existing_content}\n```\n\nPlease modify existing content so that it meets the new requirements. Your output should be plain text, without any code blocks or formatting. Do not include any explanations or comments. Just provide the final content of the file."
|
|
59
|
+
|
|
60
|
+
self.logger.debug(f"Using system prompt: {system_prompt}")
|
|
61
|
+
self.logger.debug(f"Using user prompt: {user_prompt}")
|
|
62
|
+
|
|
63
|
+
self.content = self.model_wrapper.generate_content(
|
|
64
|
+
system_prompt,
|
|
65
|
+
user_prompt,
|
|
66
|
+
dry_run=dry_run
|
|
67
|
+
)
|
|
68
|
+
self.logger.debug(f"Generated content: \n\n{self.content}")
|
|
69
|
+
|
|
70
|
+
def fetch_content(self):
|
|
71
|
+
if self.content_location:
|
|
72
|
+
self.logger.debug(f"Fetching content from: {self.content_location}")
|
|
73
|
+
try:
|
|
74
|
+
raw_content = self.content_fetcher.fetch_content(
|
|
75
|
+
self.content_location)
|
|
76
|
+
self.logger.debug(f"Fetched content: {raw_content}")
|
|
77
|
+
# Render the fetched content using the template renderer
|
|
78
|
+
template_vars = self._merge_default_template_vars(
|
|
79
|
+
self.config_variables)
|
|
80
|
+
missing_vars = self.template_renderer.prompt_for_missing_vars(
|
|
81
|
+
raw_content, template_vars)
|
|
82
|
+
template_vars.update(missing_vars)
|
|
83
|
+
self.content = self.template_renderer.render_template(
|
|
84
|
+
raw_content, template_vars)
|
|
85
|
+
self.logger.debug(f"Rendered content: {self.content}")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
self.logger.error(f"❗ Failed to fetch content from {self.content_location}: {e}")
|
|
88
|
+
|
|
89
|
+
def _merge_default_template_vars(self, template_vars):
|
|
90
|
+
default_vars = {
|
|
91
|
+
"file_name": self.name,
|
|
92
|
+
"file_directory": self.file_directory,
|
|
93
|
+
}
|
|
94
|
+
if not template_vars:
|
|
95
|
+
return default_vars
|
|
96
|
+
return {**default_vars, **template_vars}
|
|
97
|
+
|
|
98
|
+
def apply_template_variables(self, template_vars):
|
|
99
|
+
vars = self._merge_default_template_vars(template_vars)
|
|
100
|
+
self.logger.debug(f"Applying template variables: {vars}")
|
|
101
|
+
|
|
102
|
+
missing_vars = self.template_renderer.prompt_for_missing_vars(self.content, vars)
|
|
103
|
+
vars.update(missing_vars)
|
|
104
|
+
|
|
105
|
+
self.vars = vars
|
|
106
|
+
self.logger.debug(f"Final template variables: {self.vars}")
|
|
107
|
+
|
|
108
|
+
self.content = self.template_renderer.render_template(self.content, vars)
|
|
109
|
+
|
|
110
|
+
def create(self, base_path, dry_run=False, backup_path=None, file_strategy='overwrite'):
|
|
111
|
+
file_path = os.path.join(base_path, self.name)
|
|
112
|
+
|
|
113
|
+
file_path = self.template_renderer.render_template(file_path, self.vars)
|
|
114
|
+
|
|
115
|
+
# default result
|
|
116
|
+
result = {"action": None, "path": file_path}
|
|
117
|
+
|
|
118
|
+
if self.skip:
|
|
119
|
+
self.logger.info(f"⏭️ Skipped (skip=true): {file_path}")
|
|
120
|
+
result["action"] = "skipped"
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
if dry_run:
|
|
124
|
+
self.logger.info(f"[DRY RUN] Would create/update: {file_path}")
|
|
125
|
+
result["action"] = "dry_run"
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
if self.skip_if_exists and os.path.exists(file_path):
|
|
129
|
+
self.logger.info(f"⏭️ Skipped (exists and skip_if_exists=true): {file_path}")
|
|
130
|
+
result["action"] = "skipped"
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
# Create the directory if it does not exist
|
|
134
|
+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
|
135
|
+
|
|
136
|
+
existed_before = os.path.exists(file_path)
|
|
137
|
+
renamed_from = None
|
|
138
|
+
backed_up_to = None
|
|
139
|
+
|
|
140
|
+
if existed_before:
|
|
141
|
+
if file_strategy == 'backup' and backup_path:
|
|
142
|
+
backup_file_path = os.path.join(backup_path, os.path.basename(file_path))
|
|
143
|
+
shutil.copy2(file_path, backup_file_path)
|
|
144
|
+
backed_up_to = backup_file_path
|
|
145
|
+
self.logger.info(f"🗄️ Backed up: {file_path} -> {backup_file_path}")
|
|
146
|
+
elif file_strategy == 'skip':
|
|
147
|
+
self.logger.info(f"⏭️ Skipped (exists): {file_path}")
|
|
148
|
+
result["action"] = "skipped"
|
|
149
|
+
return result
|
|
150
|
+
elif file_strategy == 'append':
|
|
151
|
+
with open(file_path, 'a') as f:
|
|
152
|
+
f.write(f"{self.content}\n")
|
|
153
|
+
self.logger.info(f"📝 Appended: {file_path}")
|
|
154
|
+
result.update({"action": "appended"})
|
|
155
|
+
return result
|
|
156
|
+
elif file_strategy == 'rename':
|
|
157
|
+
new_name = f"{file_path}.{int(time.time())}"
|
|
158
|
+
os.rename(file_path, new_name)
|
|
159
|
+
renamed_from = new_name
|
|
160
|
+
self.logger.info(f"🔁 Renamed: {file_path} -> {new_name}")
|
|
161
|
+
|
|
162
|
+
# Write/overwrite the file
|
|
163
|
+
with open(file_path, 'w') as f:
|
|
164
|
+
f.write(f"{self.content}\n")
|
|
165
|
+
|
|
166
|
+
action = "created" if not existed_before else "updated"
|
|
167
|
+
if action == "created":
|
|
168
|
+
self.logger.info(f"✅ Created: {file_path}")
|
|
169
|
+
else:
|
|
170
|
+
self.logger.info(f"✅ Updated: {file_path}")
|
|
171
|
+
self.logger.debug(f"Content: \n\n{self.content}")
|
|
172
|
+
|
|
173
|
+
if self.permissions:
|
|
174
|
+
os.chmod(file_path, int(self.permissions, 8))
|
|
175
|
+
self.logger.info(f"🔐 Set permissions: {self.permissions} on {file_path}")
|
|
176
|
+
|
|
177
|
+
result.update({
|
|
178
|
+
"action": action,
|
|
179
|
+
"renamed_from": renamed_from,
|
|
180
|
+
"backed_up_to": backed_up_to,
|
|
181
|
+
})
|
|
182
|
+
return result
|
structkit/filters.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from github import Github
|
|
10
|
+
from cachetools import TTLCache, cached
|
|
11
|
+
|
|
12
|
+
cache = TTLCache(maxsize=100, ttl=600)
|
|
13
|
+
|
|
14
|
+
@cached(cache)
|
|
15
|
+
def get_latest_release(repo_name):
|
|
16
|
+
token = os.getenv('GITHUB_TOKEN')
|
|
17
|
+
|
|
18
|
+
# Use the token if available, otherwise proceed without authentication
|
|
19
|
+
if token:
|
|
20
|
+
g = Github(token)
|
|
21
|
+
else:
|
|
22
|
+
g = Github()
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
# Get the repository object
|
|
26
|
+
repo = g.get_repo(repo_name)
|
|
27
|
+
# Get the latest release
|
|
28
|
+
latest_release = repo.get_latest_release()
|
|
29
|
+
return latest_release.tag_name
|
|
30
|
+
except Exception:
|
|
31
|
+
# If an error occurs, return the default branch name
|
|
32
|
+
try:
|
|
33
|
+
default_branch = repo.default_branch
|
|
34
|
+
return default_branch
|
|
35
|
+
except Exception as e:
|
|
36
|
+
return "LATEST_RELEASE_ERROR"
|
|
37
|
+
|
|
38
|
+
@cached(cache)
|
|
39
|
+
def get_default_branch(repo_name):
|
|
40
|
+
token = os.getenv('GITHUB_TOKEN')
|
|
41
|
+
|
|
42
|
+
if token:
|
|
43
|
+
g = Github(token)
|
|
44
|
+
else:
|
|
45
|
+
g = Github()
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
repo = g.get_repo(repo_name)
|
|
49
|
+
return repo.default_branch
|
|
50
|
+
except Exception:
|
|
51
|
+
return "DEFAULT_BRANCH_ERROR"
|
|
52
|
+
|
|
53
|
+
def slugify(value):
|
|
54
|
+
# Convert to lowercase
|
|
55
|
+
value = value.lower()
|
|
56
|
+
# Replace spaces with hyphens
|
|
57
|
+
value = re.sub(r'\s+', '-', value)
|
|
58
|
+
# Remove any non-alphanumeric characters (except hyphens)
|
|
59
|
+
value = re.sub(r'[^a-z0-9-]', '', value)
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
# -----------------------------
|
|
63
|
+
# Additional helpers/filters
|
|
64
|
+
# -----------------------------
|
|
65
|
+
|
|
66
|
+
def gen_uuid() -> str:
|
|
67
|
+
return str(uuid4())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def now_iso() -> str:
|
|
71
|
+
# UTC ISO8601 string
|
|
72
|
+
return datetime.now(timezone.utc).isoformat()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def env(name: str, default: str = "") -> str:
|
|
76
|
+
return os.getenv(name, default)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def read_file(path: str, encoding: str = "utf-8") -> str:
|
|
80
|
+
try:
|
|
81
|
+
with open(path, "r", encoding=encoding) as f:
|
|
82
|
+
return f.read()
|
|
83
|
+
except Exception:
|
|
84
|
+
return ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def to_yaml(obj: Any) -> str:
|
|
88
|
+
try:
|
|
89
|
+
return yaml.safe_dump(obj, sort_keys=False)
|
|
90
|
+
except Exception:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def from_yaml(s: str) -> Any:
|
|
95
|
+
try:
|
|
96
|
+
return yaml.safe_load(s)
|
|
97
|
+
except Exception:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def to_json(obj: Any, indent: int | None = None) -> str:
|
|
102
|
+
try:
|
|
103
|
+
return json.dumps(obj, indent=indent)
|
|
104
|
+
except Exception:
|
|
105
|
+
return ""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def from_json(s: str) -> Any:
|
|
109
|
+
try:
|
|
110
|
+
return json.loads(s)
|
|
111
|
+
except Exception:
|
|
112
|
+
return None
|
structkit/input_store.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
class InputStore:
|
|
5
|
+
|
|
6
|
+
def __init__(self, input_file):
|
|
7
|
+
self.input_file = input_file
|
|
8
|
+
self.data = None
|
|
9
|
+
|
|
10
|
+
# create directory if it doesn't exist
|
|
11
|
+
directory = os.path.dirname(input_file)
|
|
12
|
+
if not os.path.exists(directory):
|
|
13
|
+
os.makedirs(directory)
|
|
14
|
+
|
|
15
|
+
# create file if it doesn't exist
|
|
16
|
+
if not os.path.exists(input_file):
|
|
17
|
+
with open(input_file, 'w') as f:
|
|
18
|
+
json.dump({}, f)
|
|
19
|
+
|
|
20
|
+
def load(self):
|
|
21
|
+
with open(self.input_file, 'r') as f:
|
|
22
|
+
self.data = json.load(f)
|
|
23
|
+
|
|
24
|
+
def get_data(self):
|
|
25
|
+
return self.data
|
|
26
|
+
|
|
27
|
+
def get_value(self, key):
|
|
28
|
+
return self.data[key]
|
|
29
|
+
|
|
30
|
+
def set_value(self, key, value):
|
|
31
|
+
self.data[key] = value
|
|
32
|
+
|
|
33
|
+
def save(self):
|
|
34
|
+
with open(self.input_file, 'w') as f:
|
|
35
|
+
json.dump(self.data, f, indent=2)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# FILE: structkit/logging_config.py
|
|
2
|
+
import logging
|
|
3
|
+
import colorlog
|
|
4
|
+
|
|
5
|
+
def configure_logging(level=logging.INFO, log_file=None):
|
|
6
|
+
"""Configure logging with colorlog."""
|
|
7
|
+
handler = colorlog.StreamHandler()
|
|
8
|
+
|
|
9
|
+
line_format = "%(log_color)s%(message)s"
|
|
10
|
+
if level == logging.DEBUG:
|
|
11
|
+
line_format = "%(log_color)s[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d] >> %(message)s"
|
|
12
|
+
|
|
13
|
+
handler.setFormatter(colorlog.ColoredFormatter(
|
|
14
|
+
line_format,
|
|
15
|
+
datefmt='%Y-%m-%d %H:%M:%S',
|
|
16
|
+
log_colors={
|
|
17
|
+
'DEBUG': 'cyan',
|
|
18
|
+
'INFO': 'green',
|
|
19
|
+
'WARNING': 'yellow',
|
|
20
|
+
'ERROR': 'red',
|
|
21
|
+
'CRITICAL': 'bold_red',
|
|
22
|
+
}
|
|
23
|
+
))
|
|
24
|
+
|
|
25
|
+
logging.basicConfig(
|
|
26
|
+
level=level,
|
|
27
|
+
handlers=[handler],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if log_file:
|
|
31
|
+
file_handler = logging.FileHandler(log_file)
|
|
32
|
+
file_handler.setFormatter(logging.Formatter(
|
|
33
|
+
"[%(asctime)s][%(levelname)s][struct] >>> %(message)s",
|
|
34
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
35
|
+
))
|
|
36
|
+
logging.getLogger().addHandler(file_handler)
|
structkit/main.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
from structkit.utils import read_config_file, merge_configs
|
|
6
|
+
from structkit.commands.generate import GenerateCommand
|
|
7
|
+
from structkit.commands.info import InfoCommand
|
|
8
|
+
from structkit.commands.validate import ValidateCommand
|
|
9
|
+
from structkit.commands.list import ListCommand
|
|
10
|
+
from structkit.commands.generate_schema import GenerateSchemaCommand
|
|
11
|
+
from structkit.commands.mcp import MCPCommand
|
|
12
|
+
from structkit.logging_config import configure_logging
|
|
13
|
+
|
|
14
|
+
# Optional dependency: shtab for static shell completion generation
|
|
15
|
+
try:
|
|
16
|
+
import shtab # type: ignore
|
|
17
|
+
except Exception: # pragma: no cover - optional at runtime
|
|
18
|
+
shtab = None
|
|
19
|
+
|
|
20
|
+
load_dotenv()
|
|
21
|
+
|
|
22
|
+
def get_parser():
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
description="Generate project structure from YAML configuration.",
|
|
25
|
+
prog="structkit",
|
|
26
|
+
epilog="Thanks for using %(prog)s! :)",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Create subparsers
|
|
30
|
+
subparsers = parser.add_subparsers()
|
|
31
|
+
|
|
32
|
+
InfoCommand(subparsers.add_parser('info', help='Show information about the package'))
|
|
33
|
+
ValidateCommand(subparsers.add_parser('validate', help='Validate the YAML configuration file'))
|
|
34
|
+
GenerateCommand(subparsers.add_parser('generate', help='Generate the project structure'))
|
|
35
|
+
ListCommand(subparsers.add_parser('list', help='List available structures'))
|
|
36
|
+
GenerateSchemaCommand(subparsers.add_parser('generate-schema', help='Generate JSON schema for available structures'))
|
|
37
|
+
MCPCommand(subparsers.add_parser('mcp', help='MCP (Model Context Protocol) support'))
|
|
38
|
+
|
|
39
|
+
# init to create a basic .struct.yaml
|
|
40
|
+
from structkit.commands.init import InitCommand
|
|
41
|
+
InitCommand(subparsers.add_parser('init', help='Initialize a basic .struct.yaml in the target directory'))
|
|
42
|
+
|
|
43
|
+
# completion manager
|
|
44
|
+
from structkit.commands.completion import CompletionCommand
|
|
45
|
+
CompletionCommand(subparsers.add_parser('completion', help='Manage shell completions'))
|
|
46
|
+
|
|
47
|
+
# Add shtab completion printing flags if available
|
|
48
|
+
if shtab is not None:
|
|
49
|
+
# Adds --print-completion and --shell flags
|
|
50
|
+
shtab.add_argument_to(parser)
|
|
51
|
+
|
|
52
|
+
return parser
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
parser = get_parser()
|
|
56
|
+
|
|
57
|
+
args = parser.parse_args()
|
|
58
|
+
|
|
59
|
+
# Check if a subcommand was provided
|
|
60
|
+
if not hasattr(args, 'func'):
|
|
61
|
+
parser.print_help()
|
|
62
|
+
parser.exit()
|
|
63
|
+
|
|
64
|
+
# Read config file if provided
|
|
65
|
+
if getattr(args, 'config_file', None):
|
|
66
|
+
file_config = read_config_file(args.config_file)
|
|
67
|
+
args = argparse.Namespace(**merge_configs(file_config, args))
|
|
68
|
+
|
|
69
|
+
# Resolve logging level precedence: STRUCTKIT_LOG_LEVEL env > --debug (if present) > --log
|
|
70
|
+
env_level = os.getenv('STRUCTKIT_LOG_LEVEL')
|
|
71
|
+
if env_level:
|
|
72
|
+
logging_level = getattr(logging, env_level.upper(), logging.INFO)
|
|
73
|
+
else:
|
|
74
|
+
# Some commands (like mcp) may add a --debug flag; respect it
|
|
75
|
+
if getattr(args, 'debug', False):
|
|
76
|
+
logging_level = logging.DEBUG
|
|
77
|
+
else:
|
|
78
|
+
logging_level = getattr(logging, getattr(args, 'log', 'INFO').upper(), logging.INFO)
|
|
79
|
+
|
|
80
|
+
configure_logging(level=logging_level, log_file=getattr(args, 'log_file', None))
|
|
81
|
+
|
|
82
|
+
args.func(args)
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|