Skip to content

Commit a63f5f7

Browse files
authored
Add named structure sources management (CLI + MCP) (#148)
1 parent 6daecbe commit a63f5f7

9 files changed

Lines changed: 573 additions & 6 deletions

File tree

docs/cli-reference.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The `struct` CLI allows you to generate project structures from YAML configurati
99
**Basic Usage:**
1010

1111
```sh
12-
structkit {info,validate,generate,explain,vars,graph,list,generate-schema,mcp,completion,init} ...
12+
structkit {info,validate,generate,explain,vars,graph,list,sources,generate-schema,mcp,completion,init} ...
1313
```
1414

1515
## Global Options
@@ -27,6 +27,7 @@ The following environment variables can be used to configure default values for
2727

2828
- `STRUCTKIT_LOG_LEVEL`: Set the default logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). Overridden by the `--log` flag.
2929
- `STRUCTKIT_STRUCTURES_PATH`: Set the default path to structure definitions. This is used as the default value for the `--structures-path` flag when not explicitly provided. When set, the CLI will log an info message indicating that this environment variable is being used.
30+
- `STRUCTKIT_SOURCES_CONFIG`: Override the user-level named sources config file (default: `$XDG_CONFIG_HOME/structkit/sources.yaml` or `~/.config/structkit/sources.yaml`).
3031

3132
**Precedence:**
3233

@@ -135,7 +136,8 @@ structkit generate
135136

136137
- `structure_definition` (optional): Path to the YAML configuration file (default: `.struct.yaml`).
137138
- `base_path` (optional): Base path where the structure will be created (default: `.`).
138-
- `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to structure definitions. Can be set via the `STRUCTKIT_STRUCTURES_PATH` environment variable. When using the environment variable (and no explicit CLI flag), an info-level log message will be emitted indicating which path is being used.
139+
- `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to structure definitions. Can be set via the `STRUCTKIT_STRUCTURES_PATH` environment variable. When using the environment variable (and no explicit CLI flag), an info-level log message will be emitted indicating which path is being used. Takes precedence over named sources.
140+
- `--source SOURCE`: Named source to use when resolving structure definitions. You can also use `<source>/<structure>` as the structure definition.
139141
- `-n INPUT_STORE, --input-store INPUT_STORE`: Path to the input store.
140142
- `-d, --dry-run`: Perform a dry run without creating any files or directories.
141143
- `--diff`: Show unified diffs for files that would be created/modified (works with `--dry-run` and in `-o console` mode).
@@ -234,12 +236,44 @@ List available structures.
234236
**Usage:**
235237

236238
```sh
237-
structkit list [-h] [-l LOG] [-c CONFIG_FILE] [-i LOG_FILE] [-s STRUCTURES_PATH]
239+
structkit list [-h] [-l LOG] [-c CONFIG_FILE] [-i LOG_FILE] [-s STRUCTURES_PATH] [--source SOURCE]
238240
```
239241

240242
**Arguments:**
241243

242-
- `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to structure definitions.
244+
- `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to structure definitions. Takes precedence over named sources.
245+
- `--source SOURCE`: Named source to list.
246+
247+
### `sources`
248+
249+
Manage named custom structure sources. Sources currently support local filesystem directories. Remote sources are reserved for future support.
250+
251+
**Usage:**
252+
253+
```sh
254+
structkit sources [--config-path CONFIG_PATH] {list,add,remove,show,validate} ...
255+
structkit sources add NAME PATH_OR_URL
256+
structkit sources remove NAME
257+
structkit sources show NAME
258+
structkit sources validate NAME
259+
structkit sources list
260+
```
261+
262+
**Arguments:**
263+
264+
- `--config-path CONFIG_PATH`: Override the sources config file for this command.
265+
- `NAME`: Source name.
266+
- `PATH_OR_URL`: Local directory to use as a structure source.
267+
268+
**Examples:**
269+
270+
```sh
271+
structkit sources add company ./templates
272+
structkit list --source company
273+
structkit generate company/project/python ./app
274+
```
275+
276+
Resolution precedence is `--structures-path`/`STRUCTKIT_STRUCTURES_PATH`, then `--source` or `<source>/<structure>`, then bundled structures.
243277

244278
### `generate-schema`
245279

docs/custom-structures.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,40 @@ For this to work, you will need to set the path to the custom structures reposit
3535
```sh
3636
structkit generate -s ~/path/to/custom-structures/structures file://.struct.yaml ./output
3737
```
38+
39+
## Named custom sources
40+
41+
StructKit can store named local structure sources in a user-level config file. This is useful when you reuse a shared template directory and do not want to pass `--structures-path` or set `STRUCTKIT_STRUCTURES_PATH` every time.
42+
43+
```bash
44+
structkit sources add company ./templates
45+
structkit sources list
46+
structkit sources show company
47+
structkit sources validate company
48+
structkit sources remove company
49+
```
50+
51+
By default, sources are written to `$XDG_CONFIG_HOME/structkit/sources.yaml` or `~/.config/structkit/sources.yaml`. Set `STRUCTKIT_SOURCES_CONFIG` to use a different file, or pass `structkit sources --config-path <file>`.
52+
53+
Named sources currently support local filesystem directories. Remote URLs are reserved for future support and are rejected by validation.
54+
55+
Use a source explicitly with `--source`:
56+
57+
```bash
58+
structkit list --source company
59+
structkit generate --source company project/python ./app
60+
```
61+
62+
You can also prefix a structure definition with the source name:
63+
64+
```bash
65+
structkit generate company/project/python ./app
66+
```
67+
68+
Source resolution precedence is:
69+
70+
1. `--structures-path` (or `STRUCTKIT_STRUCTURES_PATH`, because it populates the same CLI option)
71+
2. `--source` or a `<source>/<structure>` prefix
72+
3. Built-in StructKit structures
73+
74+
This preserves existing `STRUCTKIT_STRUCTURES_PATH` behavior and leaves `generate` and `list` unchanged unless a named source is selected.

structkit/commands/generate.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from structkit.file_item import FileItem
66
from structkit.completers import file_strategy_completer, structures_completer
77
from structkit.template_renderer import TemplateRenderer
8+
from structkit.sources import SourceError, resolve_structures_path
89

910
import subprocess
1011

@@ -21,9 +22,10 @@ def __init__(self, parser):
2122
'-s',
2223
'--structures-path',
2324
type=str,
24-
help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH)',
25+
help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH). Takes precedence over --source.',
2526
default=os.getenv('STRUCTKIT_STRUCTURES_PATH', None)
2627
)
28+
parser.add_argument('--source', type=str, help='Named source to use when resolving structure definitions')
2729
parser.add_argument('-n', '--input-store', type=str, help='Path to the input store (env: STRUCTKIT_INPUT_STORE)', default=os.getenv('STRUCTKIT_INPUT_STORE', '/tmp/structkit/input.json'))
2830
parser.add_argument('-d', '--dry-run', action='store_true', help='Perform a dry run without creating any files or directories')
2931
parser.add_argument('--diff', action='store_true', help='Show unified diffs for files that would change during dry-run or console output')
@@ -126,6 +128,16 @@ def _load_yaml_config(self, structure_definition, structures_path):
126128
return yaml.safe_load(f)
127129

128130
def execute(self, args):
131+
try:
132+
args.structures_path, args.structure_definition = resolve_structures_path(
133+
args.structures_path,
134+
getattr(args, 'source', None),
135+
args.structure_definition,
136+
)
137+
except SourceError as exc:
138+
self.logger.error(f"❗ {exc}")
139+
raise SystemExit(1) from exc
140+
129141
# Log when using STRUCTKIT_STRUCTURES_PATH environment variable
130142
if args.structures_path and args.structures_path == os.getenv('STRUCTKIT_STRUCTURES_PATH'):
131143
self.logger.info(f"Using STRUCTKIT_STRUCTURES_PATH: {args.structures_path}")

structkit/commands/list.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from structkit.commands import Command
22
import os
33
import asyncio
4+
from structkit.sources import SourceError, resolve_structures_path
45

56

67
# List command class
@@ -9,14 +10,20 @@ def __init__(self, parser):
910
super().__init__(parser)
1011
parser.description = "List available structures"
1112
parser.add_argument(
12-
'-s', '--structures-path', type=str, help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH)',
13+
'-s', '--structures-path', type=str, help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH). Takes precedence over --source.',
1314
default=os.getenv('STRUCTKIT_STRUCTURES_PATH', None)
1415
)
16+
parser.add_argument('--source', type=str, help='Named source to list')
1517
parser.add_argument('--names-only', action='store_true', help='Print only structure names, one per line (for shell completion)')
1618
parser.add_argument('--mcp', action='store_true', help='Enable MCP (Model Context Protocol) integration')
1719
parser.set_defaults(func=self.execute)
1820

1921
def execute(self, args):
22+
try:
23+
args.structures_path, _ = resolve_structures_path(args.structures_path, getattr(args, 'source', None))
24+
except SourceError as exc:
25+
self.logger.error(f"❗ {exc}")
26+
raise SystemExit(1) from exc
2027
self.logger.info("Listing available structures")
2128
if args.mcp:
2229
self._list_structures_mcp(args)

structkit/commands/sources.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from structkit.commands import Command
2+
from structkit.sources import (
3+
SourceError,
4+
add_source,
5+
get_sources_config_path,
6+
read_sources,
7+
remove_source,
8+
validate_source_path,
9+
)
10+
11+
12+
class SourcesCommand(Command):
13+
"""Manage named custom structure sources."""
14+
15+
def __init__(self, parser):
16+
super().__init__(parser)
17+
parser.description = "Manage named custom structure sources"
18+
parser.add_argument('--config-path', type=str, help='Override sources config path (env: STRUCTKIT_SOURCES_CONFIG)')
19+
subparsers = parser.add_subparsers(dest='sources_command')
20+
21+
subparsers.add_parser('list', help='List configured sources').set_defaults(sources_func=self.list_sources)
22+
23+
add_parser = subparsers.add_parser('add', help='Add or update a local source')
24+
add_parser.add_argument('name')
25+
add_parser.add_argument('path_or_url')
26+
add_parser.set_defaults(sources_func=self.add_source)
27+
28+
remove_parser = subparsers.add_parser('remove', help='Remove a configured source')
29+
remove_parser.add_argument('name')
30+
remove_parser.set_defaults(sources_func=self.remove_source)
31+
32+
show_parser = subparsers.add_parser('show', help='Show a configured source')
33+
show_parser.add_argument('name')
34+
show_parser.set_defaults(sources_func=self.show_source)
35+
36+
validate_parser = subparsers.add_parser('validate', help='Validate a configured source')
37+
validate_parser.add_argument('name')
38+
validate_parser.set_defaults(sources_func=self.validate_source)
39+
40+
parser.set_defaults(func=self.execute)
41+
42+
def execute(self, args):
43+
if not hasattr(args, 'sources_func'):
44+
self.parser.print_help()
45+
return
46+
try:
47+
args.sources_func(args)
48+
except SourceError as exc:
49+
self.logger.error(f"❗ {exc}")
50+
raise SystemExit(1) from exc
51+
52+
def list_sources(self, args):
53+
sources = read_sources(args.config_path)
54+
print(f"Sources config: {args.config_path or get_sources_config_path()}")
55+
if not sources:
56+
print("No sources configured.")
57+
return
58+
for name, path in sorted(sources.items()):
59+
print(f"{name}\t{path}")
60+
61+
def add_source(self, args):
62+
path = add_source(args.name, args.path_or_url, args.config_path)
63+
print(f"Added source '{args.name}' -> {read_sources(args.config_path)[args.name]}")
64+
print(f"Sources config: {path}")
65+
66+
def remove_source(self, args):
67+
path = remove_source(args.name, args.config_path)
68+
print(f"Removed source '{args.name}'")
69+
print(f"Sources config: {path}")
70+
71+
def show_source(self, args):
72+
sources = read_sources(args.config_path)
73+
if args.name not in sources:
74+
raise SourceError(f"source not found: {args.name}")
75+
print(f"{args.name}\t{sources[args.name]}")
76+
77+
def validate_source(self, args):
78+
sources = read_sources(args.config_path)
79+
if args.name not in sources:
80+
raise SourceError(f"source not found: {args.name}")
81+
ok, message = validate_source_path(sources[args.name])
82+
if not ok:
83+
raise SourceError(message)
84+
print(f"Source '{args.name}' is valid: {message}")

structkit/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from structkit.commands.graph import GraphCommand
1515
from structkit.commands.generate_schema import GenerateSchemaCommand
1616
from structkit.commands.mcp import MCPCommand
17+
from structkit.commands.sources import SourcesCommand
1718
from structkit.logging_config import configure_logging
1819

1920
# Optional dependency: shtab for static shell completion generation
@@ -46,6 +47,7 @@ def get_parser():
4647
GraphCommand(subparsers.add_parser('graph', help='Visualize structure dependencies'))
4748
GenerateSchemaCommand(subparsers.add_parser('generate-schema', help='Generate JSON schema for available structures'))
4849
MCPCommand(subparsers.add_parser('mcp', help='MCP (Model Context Protocol) support'))
50+
SourcesCommand(subparsers.add_parser('sources', help='Manage named custom structure sources'))
4951

5052
# init to create a basic .struct.yaml
5153
from structkit.commands.init import InitCommand

0 commit comments

Comments
 (0)