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/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from structkit.completers import log_level_completer
|
|
3
|
+
|
|
4
|
+
# Base command class
|
|
5
|
+
class Command:
|
|
6
|
+
def __init__(self, parser):
|
|
7
|
+
self.parser = parser
|
|
8
|
+
self.logger = logging.getLogger(__name__)
|
|
9
|
+
self.add_common_arguments()
|
|
10
|
+
|
|
11
|
+
def add_common_arguments(self):
|
|
12
|
+
self.parser.add_argument('-l', '--log', type=str, default='INFO', help='Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)').completer = log_level_completer
|
|
13
|
+
self.parser.add_argument('-c', '--config-file', type=str, help='Path to a configuration file')
|
|
14
|
+
self.parser.add_argument('-i', '--log-file', type=str, help='Path to a log file')
|
|
15
|
+
|
|
16
|
+
def execute(self, args):
|
|
17
|
+
raise NotImplementedError("Subclasses should implement this!")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from structkit.commands import Command
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
SUPPORTED_SHELLS = ["bash", "zsh", "fish"]
|
|
5
|
+
|
|
6
|
+
class CompletionCommand(Command):
|
|
7
|
+
def __init__(self, parser):
|
|
8
|
+
super().__init__(parser)
|
|
9
|
+
parser.description = "Manage CLI shell completions for structkit (shtab-generated)"
|
|
10
|
+
sub = parser.add_subparsers(dest="action")
|
|
11
|
+
|
|
12
|
+
install = sub.add_parser("install", help="Print the commands to enable completion for your shell")
|
|
13
|
+
install.add_argument("shell", nargs="?", choices=SUPPORTED_SHELLS, help="Shell type (auto-detected if omitted)")
|
|
14
|
+
install.set_defaults(func=self._install)
|
|
15
|
+
|
|
16
|
+
def _detect_shell(self):
|
|
17
|
+
shell = os.environ.get("SHELL", "")
|
|
18
|
+
if shell:
|
|
19
|
+
basename = os.path.basename(shell)
|
|
20
|
+
if basename in SUPPORTED_SHELLS:
|
|
21
|
+
return basename
|
|
22
|
+
# Fallback to zsh if running zsh, else bash
|
|
23
|
+
if os.environ.get("ZSH_NAME") or os.environ.get("ZDOTDIR"):
|
|
24
|
+
return "zsh"
|
|
25
|
+
return "bash"
|
|
26
|
+
|
|
27
|
+
def _install(self, args):
|
|
28
|
+
shell = args.shell or self._detect_shell()
|
|
29
|
+
print(f"Detected shell: {shell}")
|
|
30
|
+
|
|
31
|
+
if shell == "bash":
|
|
32
|
+
print("\n# Install shtab (once, in your environment):")
|
|
33
|
+
print("python -m pip install shtab")
|
|
34
|
+
print("\n# Generate static bash completion for 'struct':")
|
|
35
|
+
print("mkdir -p ~/.local/share/bash-completion/completions")
|
|
36
|
+
print("structkit --print-completion bash > ~/.local/share/bash-completion/completions/structkit")
|
|
37
|
+
print("\n# Apply now (or open a new shell):")
|
|
38
|
+
print("source ~/.bashrc")
|
|
39
|
+
|
|
40
|
+
elif shell == "zsh":
|
|
41
|
+
print("\n# Install shtab (once, in your environment):")
|
|
42
|
+
print("python -m pip install shtab")
|
|
43
|
+
print("\n# Generate static zsh completion for 'struct':")
|
|
44
|
+
print("mkdir -p ~/.zfunc")
|
|
45
|
+
print("structkit --print-completion zsh > ~/.zfunc/_structkit")
|
|
46
|
+
print("\n# Ensure zsh loads user functions/completions (append to ~/.zshrc if needed):")
|
|
47
|
+
print('echo "fpath=(~/.zfunc $fpath)" >> ~/.zshrc')
|
|
48
|
+
print('echo "autoload -U compinit && compinit" >> ~/.zshrc')
|
|
49
|
+
print("\n# Apply now (or open a new shell):")
|
|
50
|
+
print("exec zsh")
|
|
51
|
+
|
|
52
|
+
elif shell == "fish":
|
|
53
|
+
print("\n# Install shtab (once, in your environment):")
|
|
54
|
+
print("python -m pip install shtab")
|
|
55
|
+
print("\n# Generate static fish completion for 'struct':")
|
|
56
|
+
print('mkdir -p ~/.config/fish/completions')
|
|
57
|
+
print('structkit --print-completion fish > ~/.config/fish/completions/structkit.fish')
|
|
58
|
+
print("\n# Apply now:")
|
|
59
|
+
print("fish -c 'source ~/.config/fish/completions/structkit.fish'")
|
|
60
|
+
|
|
61
|
+
else:
|
|
62
|
+
self.logger.error(f"Unsupported shell: {shell}. Supported: {', '.join(SUPPORTED_SHELLS)}")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
print("\nTip: You can also print completion directly via: structkit --print-completion <shell>")
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
from structkit.commands import Command
|
|
2
|
+
import os
|
|
3
|
+
import yaml
|
|
4
|
+
import argparse
|
|
5
|
+
from structkit.file_item import FileItem
|
|
6
|
+
from structkit.completers import file_strategy_completer, structures_completer
|
|
7
|
+
from structkit.template_renderer import TemplateRenderer
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
|
|
11
|
+
# Generate command class
|
|
12
|
+
class GenerateCommand(Command):
|
|
13
|
+
def __init__(self, parser):
|
|
14
|
+
super().__init__(parser)
|
|
15
|
+
parser.description = "Generate the project structure from a YAML configuration file"
|
|
16
|
+
structure_arg = parser.add_argument('structure_definition', nargs='?', default='.struct.yaml', type=str, help='Path to the YAML configuration file (default: .struct.yaml)')
|
|
17
|
+
structure_arg.completer = structures_completer
|
|
18
|
+
parser.add_argument('base_path', nargs='?', default='.', type=str, help='Base path where the structure will be created (default: current directory)')
|
|
19
|
+
parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions')
|
|
20
|
+
parser.add_argument('-n', '--input-store', type=str, help='Path to the input store', default='/tmp/structkit/input.json')
|
|
21
|
+
parser.add_argument('-d', '--dry-run', action='store_true', help='Perform a dry run without creating any files or directories')
|
|
22
|
+
parser.add_argument('--diff', action='store_true', help='Show unified diffs for files that would change during dry-run or console output')
|
|
23
|
+
parser.add_argument('-v', '--vars', type=str, help='Template variables in the format KEY1=value1,KEY2=value2')
|
|
24
|
+
parser.add_argument('-b', '--backup', type=str, help='Path to the backup folder')
|
|
25
|
+
parser.add_argument('-f', '--file-strategy', type=str, choices=['overwrite', 'skip', 'append', 'rename', 'backup'], default='overwrite', help='Strategy for handling existing files').completer = file_strategy_completer
|
|
26
|
+
parser.add_argument('-p', '--global-system-prompt', type=str, help='Global system prompt for OpenAI')
|
|
27
|
+
parser.add_argument('--non-interactive', action='store_true', help='Run the command in non-interactive mode')
|
|
28
|
+
parser.add_argument('--mappings-file', type=str, action='append',
|
|
29
|
+
help='Path to a YAML file containing mappings to be used in templates (can be specified multiple times)')
|
|
30
|
+
parser.add_argument('-o', '--output', type=str,
|
|
31
|
+
choices=['console', 'file'], default='file', help='Output mode')
|
|
32
|
+
parser.set_defaults(func=self.execute)
|
|
33
|
+
|
|
34
|
+
def _parse_template_vars(self, vars_str):
|
|
35
|
+
"""Parse a comma-separated KEY=VALUE string into a dict safely.
|
|
36
|
+
- Ignores empty tokens and trailing commas
|
|
37
|
+
- Supports values containing '=' by splitting only on the first '='
|
|
38
|
+
- Logs and skips malformed entries without raising
|
|
39
|
+
"""
|
|
40
|
+
result = {}
|
|
41
|
+
if not vars_str:
|
|
42
|
+
return result
|
|
43
|
+
# Normalize by removing accidental leading/trailing commas and whitespace
|
|
44
|
+
tokens = [t.strip() for t in vars_str.strip(', ').split(',')]
|
|
45
|
+
for token in tokens:
|
|
46
|
+
if not token:
|
|
47
|
+
continue
|
|
48
|
+
if '=' not in token:
|
|
49
|
+
# Skip malformed item but warn
|
|
50
|
+
self.logger.warning(f"Skipping malformed template var (no '='): '{token}'")
|
|
51
|
+
continue
|
|
52
|
+
key, value = token.split('=', 1)
|
|
53
|
+
key = key.strip()
|
|
54
|
+
value = value
|
|
55
|
+
if not key:
|
|
56
|
+
self.logger.warning(f"Skipping template var with empty key: '{token}'")
|
|
57
|
+
continue
|
|
58
|
+
result[key] = value
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
def _deep_merge_dicts(self, dict1, dict2):
|
|
62
|
+
"""
|
|
63
|
+
Deep merge two dictionaries, with dict2 values overriding dict1 values.
|
|
64
|
+
"""
|
|
65
|
+
result = dict1.copy()
|
|
66
|
+
for key, value in dict2.items():
|
|
67
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
68
|
+
result[key] = self._deep_merge_dicts(result[key], value)
|
|
69
|
+
else:
|
|
70
|
+
result[key] = value
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
def _run_hooks(self, hooks, hook_type="pre"): # helper for running hooks
|
|
74
|
+
if not hooks:
|
|
75
|
+
return True
|
|
76
|
+
for cmd in hooks:
|
|
77
|
+
self.logger.info(f"Running {hook_type}-hook: {cmd}")
|
|
78
|
+
try:
|
|
79
|
+
result = subprocess.run(cmd, shell=True, check=True, capture_output=True, text=True)
|
|
80
|
+
if result.stdout:
|
|
81
|
+
self.logger.info(f"{hook_type}-hook stdout: {result.stdout.strip()}")
|
|
82
|
+
if result.stderr:
|
|
83
|
+
self.logger.info(f"{hook_type}-hook stderr: {result.stderr.strip()}")
|
|
84
|
+
except subprocess.CalledProcessError as e:
|
|
85
|
+
self.logger.error(f"{hook_type}-hook failed: {cmd}")
|
|
86
|
+
self.logger.error(f"Return code: {e.returncode}")
|
|
87
|
+
if e.stdout:
|
|
88
|
+
self.logger.error(f"stdout: {e.stdout.strip()}")
|
|
89
|
+
if e.stderr:
|
|
90
|
+
self.logger.error(f"stderr: {e.stderr.strip()}")
|
|
91
|
+
return False
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def _load_yaml_config(self, structure_definition, structures_path):
|
|
95
|
+
if structure_definition.endswith(".yaml") and not structure_definition.startswith("file://"):
|
|
96
|
+
structure_definition = f"file://{structure_definition}"
|
|
97
|
+
|
|
98
|
+
if structure_definition.startswith("file://") and structure_definition.endswith(".yaml"):
|
|
99
|
+
with open(structure_definition[7:], 'r') as f:
|
|
100
|
+
return yaml.safe_load(f)
|
|
101
|
+
else:
|
|
102
|
+
this_file = os.path.dirname(os.path.realpath(__file__))
|
|
103
|
+
contribs_path = os.path.join(this_file, "..", "contribs")
|
|
104
|
+
file_path = os.path.join(contribs_path, f"{structure_definition}.yaml")
|
|
105
|
+
if structures_path:
|
|
106
|
+
file_path = os.path.join(structures_path, f"{structure_definition}.yaml")
|
|
107
|
+
if not os.path.exists(file_path):
|
|
108
|
+
file_path = os.path.join(contribs_path, f"{structure_definition}.yaml")
|
|
109
|
+
if not os.path.exists(file_path):
|
|
110
|
+
self.logger.error(f"❗ File not found: {file_path}")
|
|
111
|
+
return None
|
|
112
|
+
with open(file_path, 'r') as f:
|
|
113
|
+
return yaml.safe_load(f)
|
|
114
|
+
|
|
115
|
+
def execute(self, args):
|
|
116
|
+
self.logger.info(f"Generating structure")
|
|
117
|
+
self.logger.info(f" Structure definition: {args.structure_definition}")
|
|
118
|
+
self.logger.info(f" Base path: {args.base_path}")
|
|
119
|
+
|
|
120
|
+
# Load mappings if provided
|
|
121
|
+
mappings = {}
|
|
122
|
+
if getattr(args, 'mappings_file', None):
|
|
123
|
+
for mappings_file_path in args.mappings_file:
|
|
124
|
+
if os.path.exists(mappings_file_path):
|
|
125
|
+
self.logger.info(f"Loading mappings from: {mappings_file_path}")
|
|
126
|
+
with open(mappings_file_path, 'r') as mf:
|
|
127
|
+
try:
|
|
128
|
+
file_mappings = yaml.safe_load(mf) or {}
|
|
129
|
+
# Deep merge the mappings, with later files overriding earlier ones
|
|
130
|
+
mappings = self._deep_merge_dicts(mappings, file_mappings)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
self.logger.error(f"Failed to load mappings file {mappings_file_path}: {e}")
|
|
133
|
+
return
|
|
134
|
+
else:
|
|
135
|
+
self.logger.error(f"Mappings file not found: {mappings_file_path}")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if args.backup and not os.path.exists(args.backup):
|
|
139
|
+
os.makedirs(args.backup)
|
|
140
|
+
|
|
141
|
+
if args.base_path and not os.path.exists(args.base_path) and "console" not in args.output:
|
|
142
|
+
self.logger.info(f"Creating base path: {args.base_path}")
|
|
143
|
+
os.makedirs(args.base_path)
|
|
144
|
+
|
|
145
|
+
# Load config to check for hooks
|
|
146
|
+
config = None
|
|
147
|
+
config = self._load_yaml_config(args.structure_definition, args.structures_path)
|
|
148
|
+
if config is None:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
pre_hooks = config.get('pre_hooks', [])
|
|
152
|
+
post_hooks = config.get('post_hooks', [])
|
|
153
|
+
|
|
154
|
+
# Run pre-hooks
|
|
155
|
+
if not self._run_hooks(pre_hooks, hook_type="pre"):
|
|
156
|
+
self.logger.error("Aborting generation due to pre-hook failure.")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
# Actually generate structure
|
|
160
|
+
self._create_structure(args, mappings)
|
|
161
|
+
|
|
162
|
+
# Run post-hooks
|
|
163
|
+
if not self._run_hooks(post_hooks, hook_type="post"):
|
|
164
|
+
self.logger.error("Post-hook failed.")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
def _create_structure(self, args, mappings=None, summary=None, print_summary=True):
|
|
168
|
+
if isinstance(args, dict):
|
|
169
|
+
args = argparse.Namespace(**args)
|
|
170
|
+
this_file = os.path.dirname(os.path.realpath(__file__))
|
|
171
|
+
contribs_path = os.path.join(this_file, "..", "contribs")
|
|
172
|
+
|
|
173
|
+
config = self._load_yaml_config(args.structure_definition, args.structures_path)
|
|
174
|
+
if config is None:
|
|
175
|
+
return summary if summary is not None else None
|
|
176
|
+
|
|
177
|
+
# Safely parse template variables
|
|
178
|
+
template_vars = self._parse_template_vars(args.vars) if getattr(args, 'vars', None) else {}
|
|
179
|
+
config_structure = config.get('files', config.get('structure', []))
|
|
180
|
+
config_folders = config.get('folders', [])
|
|
181
|
+
config_variables = config.get('variables', [])
|
|
182
|
+
|
|
183
|
+
# Action counters for final summary (initialize once and reuse across recursive calls)
|
|
184
|
+
if summary is None:
|
|
185
|
+
summary = {
|
|
186
|
+
"created": 0,
|
|
187
|
+
"updated": 0,
|
|
188
|
+
"appended": 0,
|
|
189
|
+
"skipped": 0,
|
|
190
|
+
"backed_up": 0,
|
|
191
|
+
"renamed": 0,
|
|
192
|
+
"folders": 0,
|
|
193
|
+
"dry_run_created": 0,
|
|
194
|
+
"dry_run_updated": 0,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for item in config_structure:
|
|
198
|
+
self.logger.debug(f"Processing item: {item}")
|
|
199
|
+
for name, content in item.items():
|
|
200
|
+
self.logger.debug(f"Processing name: {name}, content: {content}")
|
|
201
|
+
if isinstance(content, dict):
|
|
202
|
+
content["name"] = name
|
|
203
|
+
content["global_system_prompt"] = args.global_system_prompt
|
|
204
|
+
content["config_variables"] = config_variables
|
|
205
|
+
content["input_store"] = args.input_store
|
|
206
|
+
content["non_interactive"] = args.non_interactive
|
|
207
|
+
content["mappings"] = mappings or {}
|
|
208
|
+
file_item = FileItem(content)
|
|
209
|
+
file_item.fetch_content()
|
|
210
|
+
elif isinstance(content, str):
|
|
211
|
+
file_item = FileItem(
|
|
212
|
+
{
|
|
213
|
+
"name": name,
|
|
214
|
+
"content": content,
|
|
215
|
+
"config_variables": config_variables,
|
|
216
|
+
"input_store": args.input_store,
|
|
217
|
+
"non_interactive": args.non_interactive,
|
|
218
|
+
"mappings": mappings or {},
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Determine the full file path
|
|
223
|
+
file_path_to_create = os.path.join(args.base_path, name)
|
|
224
|
+
existing_content = None
|
|
225
|
+
if os.path.exists(file_path_to_create):
|
|
226
|
+
self.logger.info(f"ℹ️ Exists: {file_path_to_create}")
|
|
227
|
+
with open(file_path_to_create, 'r') as existing_file:
|
|
228
|
+
existing_content = existing_file.read()
|
|
229
|
+
|
|
230
|
+
file_item.process_prompt(
|
|
231
|
+
args.dry_run,
|
|
232
|
+
existing_content=existing_content
|
|
233
|
+
)
|
|
234
|
+
file_item.apply_template_variables(template_vars)
|
|
235
|
+
|
|
236
|
+
# Output mode logic with diff support
|
|
237
|
+
if hasattr(args, 'output') and args.output == 'console':
|
|
238
|
+
print(f"=== {file_path_to_create} ===")
|
|
239
|
+
if args.diff and existing_content is not None:
|
|
240
|
+
import difflib
|
|
241
|
+
new_content = file_item.content if file_item.content.endswith("\n") else file_item.content + "\n"
|
|
242
|
+
old_content = existing_content if existing_content.endswith("\n") else existing_content + "\n"
|
|
243
|
+
diff = difflib.unified_diff(
|
|
244
|
+
old_content.splitlines(keepends=True),
|
|
245
|
+
new_content.splitlines(keepends=True),
|
|
246
|
+
fromfile=f"a/{file_path_to_create}",
|
|
247
|
+
tofile=f"b/{file_path_to_create}",
|
|
248
|
+
)
|
|
249
|
+
print("".join(diff))
|
|
250
|
+
else:
|
|
251
|
+
print(file_item.content)
|
|
252
|
+
else:
|
|
253
|
+
# When dry-run with --diff and files mode, print action and diff instead of writing
|
|
254
|
+
if args.dry_run and args.diff:
|
|
255
|
+
action = "create"
|
|
256
|
+
if existing_content is not None:
|
|
257
|
+
action = "update"
|
|
258
|
+
print(f"[DRY RUN] {action}: {file_path_to_create}")
|
|
259
|
+
if action == "create":
|
|
260
|
+
summary["dry_run_created"] += 1
|
|
261
|
+
else:
|
|
262
|
+
summary["dry_run_updated"] += 1
|
|
263
|
+
import difflib
|
|
264
|
+
new_content = file_item.content if file_item.content.endswith("\n") else file_item.content + "\n"
|
|
265
|
+
old_content = (existing_content if existing_content is not None else "")
|
|
266
|
+
old_content = old_content if old_content.endswith("\n") else (old_content + ("\n" if old_content else ""))
|
|
267
|
+
diff = difflib.unified_diff(
|
|
268
|
+
old_content.splitlines(keepends=True),
|
|
269
|
+
new_content.splitlines(keepends=True),
|
|
270
|
+
fromfile=f"a/{file_path_to_create}",
|
|
271
|
+
tofile=f"b/{file_path_to_create}",
|
|
272
|
+
)
|
|
273
|
+
print("".join(diff))
|
|
274
|
+
else:
|
|
275
|
+
result = file_item.create(
|
|
276
|
+
args.base_path,
|
|
277
|
+
args.dry_run or False,
|
|
278
|
+
args.backup or None,
|
|
279
|
+
args.file_strategy or 'overwrite'
|
|
280
|
+
)
|
|
281
|
+
if isinstance(result, dict):
|
|
282
|
+
if result.get("action") == "created":
|
|
283
|
+
summary["created"] += 1
|
|
284
|
+
elif result.get("action") == "updated":
|
|
285
|
+
summary["updated"] += 1
|
|
286
|
+
elif result.get("action") == "appended":
|
|
287
|
+
summary["appended"] += 1
|
|
288
|
+
elif result.get("action") == "skipped":
|
|
289
|
+
summary["skipped"] += 1
|
|
290
|
+
if result.get("backed_up_to"):
|
|
291
|
+
summary["backed_up"] += 1
|
|
292
|
+
if result.get("renamed_from"):
|
|
293
|
+
summary["renamed"] += 1
|
|
294
|
+
|
|
295
|
+
for item in config_folders:
|
|
296
|
+
for folder, content in item.items():
|
|
297
|
+
folder_path = os.path.join(args.base_path, folder)
|
|
298
|
+
if hasattr(args, 'output') and args.output == 'file':
|
|
299
|
+
os.makedirs(folder_path, exist_ok=True)
|
|
300
|
+
self.logger.info(f"📁 Created folder: {folder_path}")
|
|
301
|
+
summary["folders"] += 1
|
|
302
|
+
|
|
303
|
+
# check if content has structkit value
|
|
304
|
+
if 'struct' in content:
|
|
305
|
+
self.logger.info(f"Generating structure")
|
|
306
|
+
self.logger.info(f" Folder: {folder}")
|
|
307
|
+
self.logger.info(f" Struct(s):")
|
|
308
|
+
if isinstance(content['struct'], list):
|
|
309
|
+
# iterate over the list of structures
|
|
310
|
+
for struct in content['struct']:
|
|
311
|
+
self.logger.info(f" - {struct}")
|
|
312
|
+
if isinstance(content['struct'], str):
|
|
313
|
+
self.logger.info(f" - {content['struct']}")
|
|
314
|
+
|
|
315
|
+
# get vars from with param. this will be a dict of key value pairs
|
|
316
|
+
merged_vars = ""
|
|
317
|
+
|
|
318
|
+
# dict to comma separated string
|
|
319
|
+
if 'with' in content:
|
|
320
|
+
if isinstance(content['with'], dict):
|
|
321
|
+
# Render Jinja2 expressions in each value using TemplateRenderer
|
|
322
|
+
rendered_with = {}
|
|
323
|
+
renderer = TemplateRenderer(
|
|
324
|
+
config_variables, args.input_store, args.non_interactive, mappings)
|
|
325
|
+
for k, v in content['with'].items():
|
|
326
|
+
# Render the value as a template, passing in mappings and template_vars
|
|
327
|
+
context = template_vars.copy() if template_vars else {}
|
|
328
|
+
context['mappings'] = mappings or {}
|
|
329
|
+
rendered_with[k] = renderer.render_template(str(v), context)
|
|
330
|
+
merged_vars = ",".join(
|
|
331
|
+
[f"{k}={v}" for k, v in rendered_with.items()])
|
|
332
|
+
|
|
333
|
+
# Merge parent args.vars safely without introducing trailing commas
|
|
334
|
+
if getattr(args, 'vars', None):
|
|
335
|
+
parts = []
|
|
336
|
+
parent_vars = args.vars.strip().strip(',')
|
|
337
|
+
if parent_vars:
|
|
338
|
+
parts.append(parent_vars)
|
|
339
|
+
if merged_vars:
|
|
340
|
+
parts.append(merged_vars)
|
|
341
|
+
merged_vars = ",".join(parts)
|
|
342
|
+
|
|
343
|
+
# If nothing to merge, keep None to avoid accidental truthiness with empty string
|
|
344
|
+
merged_vars = merged_vars if merged_vars else None
|
|
345
|
+
|
|
346
|
+
if isinstance(content['struct'], str):
|
|
347
|
+
self._create_structure({
|
|
348
|
+
'structure_definition': content['struct'],
|
|
349
|
+
'base_path': folder_path,
|
|
350
|
+
'structures_path': args.structures_path,
|
|
351
|
+
'dry_run': args.dry_run,
|
|
352
|
+
'diff': getattr(args, 'diff', False),
|
|
353
|
+
'output': getattr(args, 'output', 'file'),
|
|
354
|
+
'vars': merged_vars,
|
|
355
|
+
'backup': args.backup,
|
|
356
|
+
'file_strategy': args.file_strategy,
|
|
357
|
+
'global_system_prompt': args.global_system_prompt,
|
|
358
|
+
'input_store': args.input_store,
|
|
359
|
+
'non_interactive': args.non_interactive,
|
|
360
|
+
}, mappings=mappings, summary=summary, print_summary=False)
|
|
361
|
+
elif isinstance(content['struct'], list):
|
|
362
|
+
for struct in content['struct']:
|
|
363
|
+
self._create_structure({
|
|
364
|
+
'structure_definition': struct,
|
|
365
|
+
'base_path': folder_path,
|
|
366
|
+
'structures_path': args.structures_path,
|
|
367
|
+
'dry_run': args.dry_run,
|
|
368
|
+
'diff': getattr(args, 'diff', False),
|
|
369
|
+
'output': getattr(args, 'output', 'file'),
|
|
370
|
+
'vars': merged_vars,
|
|
371
|
+
'backup': args.backup,
|
|
372
|
+
'file_strategy': args.file_strategy,
|
|
373
|
+
'global_system_prompt': args.global_system_prompt,
|
|
374
|
+
'input_store': args.input_store,
|
|
375
|
+
'non_interactive': args.non_interactive,
|
|
376
|
+
}, mappings=mappings, summary=summary, print_summary=False)
|
|
377
|
+
else:
|
|
378
|
+
self.logger.warning(f"Unsupported content in folder: {folder}")
|
|
379
|
+
|
|
380
|
+
# Final summary (only once for top-level call)
|
|
381
|
+
if print_summary:
|
|
382
|
+
self.logger.info("")
|
|
383
|
+
self.logger.info("Summary of actions:")
|
|
384
|
+
self.logger.info(f" ✅ Created: {summary['created']}")
|
|
385
|
+
self.logger.info(f" ✅ Updated: {summary['updated']}")
|
|
386
|
+
self.logger.info(f" 📝 Appended: {summary['appended']}")
|
|
387
|
+
self.logger.info(f" ⏭️ Skipped: {summary['skipped']}")
|
|
388
|
+
self.logger.info(f" 🗄️ Backed up: {summary['backed_up']}")
|
|
389
|
+
self.logger.info(f" 🔁 Renamed: {summary['renamed']}")
|
|
390
|
+
self.logger.info(f" 📁 Folders created: {summary['folders']}")
|
|
391
|
+
if args.dry_run:
|
|
392
|
+
self.logger.info(
|
|
393
|
+
f" [DRY RUN] Would create: {summary['dry_run_created']}")
|
|
394
|
+
self.logger.info(
|
|
395
|
+
f" [DRY RUN] Would update: {summary['dry_run_updated']}")
|
|
396
|
+
|
|
397
|
+
return summary
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from structkit.commands import Command
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
# Generate Schema command class
|
|
6
|
+
class GenerateSchemaCommand(Command):
|
|
7
|
+
def __init__(self, parser):
|
|
8
|
+
super().__init__(parser)
|
|
9
|
+
parser.description = "Generate JSON schema for available structures"
|
|
10
|
+
parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions')
|
|
11
|
+
parser.add_argument('-o', '--output', type=str, help='Output file path for the schema (default: stdout)')
|
|
12
|
+
parser.set_defaults(func=self.execute)
|
|
13
|
+
|
|
14
|
+
def execute(self, args):
|
|
15
|
+
self.logger.info("Generating JSON schema for available structures")
|
|
16
|
+
self._generate_schema(args)
|
|
17
|
+
|
|
18
|
+
def _generate_schema(self, args):
|
|
19
|
+
# Get the path to contribs directory (built-in structures)
|
|
20
|
+
this_file = os.path.dirname(os.path.realpath(__file__))
|
|
21
|
+
contribs_path = os.path.join(this_file, "..", "contribs")
|
|
22
|
+
|
|
23
|
+
# Determine paths to scan
|
|
24
|
+
if args.structures_path:
|
|
25
|
+
final_path = args.structures_path
|
|
26
|
+
paths_to_list = [final_path, contribs_path]
|
|
27
|
+
else:
|
|
28
|
+
paths_to_list = [contribs_path]
|
|
29
|
+
|
|
30
|
+
# Collect all available structures
|
|
31
|
+
all_structures = set()
|
|
32
|
+
for path in paths_to_list:
|
|
33
|
+
if os.path.exists(path):
|
|
34
|
+
for root, _, files in os.walk(path):
|
|
35
|
+
for file in files:
|
|
36
|
+
if file.endswith(".yaml"):
|
|
37
|
+
file_path = os.path.join(root, file)
|
|
38
|
+
rel_path = os.path.relpath(file_path, path)
|
|
39
|
+
# Remove .yaml extension
|
|
40
|
+
rel_path = rel_path[:-5]
|
|
41
|
+
all_structures.add(rel_path)
|
|
42
|
+
|
|
43
|
+
# Create JSON schema
|
|
44
|
+
schema = {
|
|
45
|
+
"definitions": {
|
|
46
|
+
"PluginList": {
|
|
47
|
+
"enum": sorted(list(all_structures))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Convert to JSON string
|
|
53
|
+
json_output = json.dumps(schema, indent=2)
|
|
54
|
+
|
|
55
|
+
# Output to file or stdout
|
|
56
|
+
if args.output:
|
|
57
|
+
# Create output directory if it doesn't exist
|
|
58
|
+
output_dir = os.path.dirname(args.output)
|
|
59
|
+
if output_dir and not os.path.exists(output_dir):
|
|
60
|
+
os.makedirs(output_dir)
|
|
61
|
+
|
|
62
|
+
with open(args.output, 'w') as f:
|
|
63
|
+
f.write(json_output)
|
|
64
|
+
self.logger.info(f"Schema written to {args.output}")
|
|
65
|
+
print(f"✅ Schema successfully generated at: {args.output}")
|
|
66
|
+
else:
|
|
67
|
+
print(json_output)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from structkit.commands import Command
|
|
2
|
+
import os
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
# Import command class
|
|
6
|
+
# This class is responsible for importing a structure definition given a file path. It will create the structure in the base path provided.
|
|
7
|
+
class ImportCommand(Command):
|
|
8
|
+
def __init__(self, parser):
|
|
9
|
+
super().__init__(parser)
|
|
10
|
+
parser.add_argument('import_from', type=str, help='Directory to import the structure from')
|
|
11
|
+
parser.add_argument('-o', '--output-path', type=str, help='Path to the output directory', default='.')
|
|
12
|
+
parser.set_defaults(func=self.execute)
|
|
13
|
+
|
|
14
|
+
def execute(self, args):
|
|
15
|
+
self.logger.info(f"Importing structure from {args.import_from} to {args.output_path}")
|
|
16
|
+
|
|
17
|
+
self._import_structure(args)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _import_structure(self, args):
|
|
21
|
+
raise NotImplementedError("This method needs to be implemented")
|
|
22
|
+
# # Check if the import_from directory exists
|
|
23
|
+
# if not os.path.exists(args.import_from):
|
|
24
|
+
# self.logger.error(f"Directory not found: {args.import_from}")
|
|
25
|
+
# return
|
|
26
|
+
|
|
27
|
+
# # Define the path for the structure.yaml file
|
|
28
|
+
# structure_definition = os.path.join(args.output_path, 'structure.yaml')
|
|
29
|
+
|
|
30
|
+
# # Check if the output_path exists, if not, create it
|
|
31
|
+
# if not os.path.exists(args.output_path):
|
|
32
|
+
# self.logger.warning(f"Output path not found: {args.output_path}, creating it")
|
|
33
|
+
# os.makedirs(args.output_path)
|
|
34
|
+
|
|
35
|
+
# # Check if the structure.yaml file already exists
|
|
36
|
+
# if os.path.exists(structure_definition):
|
|
37
|
+
# self.logger.warning(f"Structure definition already exists at {structure_definition}")
|
|
38
|
+
# # Ask the user if they want to overwrite the existing structure.yaml file
|
|
39
|
+
# if input("Do you want to overwrite it? (y/N) ").lower() != 'y':
|
|
40
|
+
# return
|
|
41
|
+
|
|
42
|
+
# # Initialize the structure dictionary
|
|
43
|
+
# generated_structure = {
|
|
44
|
+
# 'structure': [],
|
|
45
|
+
# 'folders': [],
|
|
46
|
+
# 'variables': []
|
|
47
|
+
# }
|
|
48
|
+
|
|
49
|
+
# for root, dirs, files in os.walk(args.import_from):
|
|
50
|
+
# for file in files:
|
|
51
|
+
# self.logger.info(f"Processing file {file}")
|
|
52
|
+
# file_path = os.path.join(root, file)
|
|
53
|
+
# relative_path = os.path.relpath(file_path, args.import_from)
|
|
54
|
+
# generated_structure['structure'].append(
|
|
55
|
+
# {
|
|
56
|
+
# 'path': relative_path,
|
|
57
|
+
# 'content': open(file_path, 'r').read()
|
|
58
|
+
# }
|
|
59
|
+
# )
|
|
60
|
+
|
|
61
|
+
# with open(structure_definition, 'w') as f:
|
|
62
|
+
# self.logger.info(f"Writing structure definition to {structure_definition}")
|
|
63
|
+
# yaml.dump(generated_structure, f)
|