Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The `struct` CLI allows you to generate project structures from YAML configurati
**Basic Usage:**

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

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

- `STRUCTKIT_LOG_LEVEL`: Set the default logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). Overridden by the `--log` flag.
- `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.
- `STRUCTKIT_SOURCES_CONFIG`: Override the user-level named sources config file (default: `$XDG_CONFIG_HOME/structkit/sources.yaml` or `~/.config/structkit/sources.yaml`).

**Precedence:**

Expand Down Expand Up @@ -135,7 +136,8 @@ structkit generate

- `structure_definition` (optional): Path to the YAML configuration file (default: `.struct.yaml`).
- `base_path` (optional): Base path where the structure will be created (default: `.`).
- `-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.
- `-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.
- `--source SOURCE`: Named source to use when resolving structure definitions. You can also use `<source>/<structure>` as the structure definition.
- `-n INPUT_STORE, --input-store INPUT_STORE`: Path to the input store.
- `-d, --dry-run`: Perform a dry run without creating any files or directories.
- `--diff`: Show unified diffs for files that would be created/modified (works with `--dry-run` and in `-o console` mode).
Expand Down Expand Up @@ -234,12 +236,44 @@ List available structures.
**Usage:**

```sh
structkit list [-h] [-l LOG] [-c CONFIG_FILE] [-i LOG_FILE] [-s STRUCTURES_PATH]
structkit list [-h] [-l LOG] [-c CONFIG_FILE] [-i LOG_FILE] [-s STRUCTURES_PATH] [--source SOURCE]
```

**Arguments:**

- `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to structure definitions.
- `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to structure definitions. Takes precedence over named sources.
- `--source SOURCE`: Named source to list.

### `sources`

Manage named custom structure sources. Sources currently support local filesystem directories. Remote sources are reserved for future support.

**Usage:**

```sh
structkit sources [--config-path CONFIG_PATH] {list,add,remove,show,validate} ...
structkit sources add NAME PATH_OR_URL
structkit sources remove NAME
structkit sources show NAME
structkit sources validate NAME
structkit sources list
```

**Arguments:**

- `--config-path CONFIG_PATH`: Override the sources config file for this command.
- `NAME`: Source name.
- `PATH_OR_URL`: Local directory to use as a structure source.

**Examples:**

```sh
structkit sources add company ./templates
structkit list --source company
structkit generate company/project/python ./app
```

Resolution precedence is `--structures-path`/`STRUCTKIT_STRUCTURES_PATH`, then `--source` or `<source>/<structure>`, then bundled structures.

### `generate-schema`

Expand Down
37 changes: 37 additions & 0 deletions docs/custom-structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,40 @@ For this to work, you will need to set the path to the custom structures reposit
```sh
structkit generate -s ~/path/to/custom-structures/structures file://.struct.yaml ./output
```

## Named custom sources

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.

```bash
structkit sources add company ./templates
structkit sources list
structkit sources show company
structkit sources validate company
structkit sources remove company
```

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>`.

Named sources currently support local filesystem directories. Remote URLs are reserved for future support and are rejected by validation.

Use a source explicitly with `--source`:

```bash
structkit list --source company
structkit generate --source company project/python ./app
```

You can also prefix a structure definition with the source name:

```bash
structkit generate company/project/python ./app
```

Source resolution precedence is:

1. `--structures-path` (or `STRUCTKIT_STRUCTURES_PATH`, because it populates the same CLI option)
2. `--source` or a `<source>/<structure>` prefix
3. Built-in StructKit structures

This preserves existing `STRUCTKIT_STRUCTURES_PATH` behavior and leaves `generate` and `list` unchanged unless a named source is selected.
14 changes: 13 additions & 1 deletion structkit/commands/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from structkit.file_item import FileItem
from structkit.completers import file_strategy_completer, structures_completer
from structkit.template_renderer import TemplateRenderer
from structkit.sources import SourceError, resolve_structures_path

import subprocess

Expand All @@ -21,9 +22,10 @@ def __init__(self, parser):
'-s',
'--structures-path',
type=str,
help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH)',
help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH). Takes precedence over --source.',
default=os.getenv('STRUCTKIT_STRUCTURES_PATH', None)
)
parser.add_argument('--source', type=str, help='Named source to use when resolving structure definitions')
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'))
parser.add_argument('-d', '--dry-run', action='store_true', help='Perform a dry run without creating any files or directories')
parser.add_argument('--diff', action='store_true', help='Show unified diffs for files that would change during dry-run or console output')
Expand Down Expand Up @@ -126,6 +128,16 @@ def _load_yaml_config(self, structure_definition, structures_path):
return yaml.safe_load(f)

def execute(self, args):
try:
args.structures_path, args.structure_definition = resolve_structures_path(
args.structures_path,
getattr(args, 'source', None),
args.structure_definition,
)
except SourceError as exc:
self.logger.error(f"❗ {exc}")
raise SystemExit(1) from exc

# Log when using STRUCTKIT_STRUCTURES_PATH environment variable
if args.structures_path and args.structures_path == os.getenv('STRUCTKIT_STRUCTURES_PATH'):
self.logger.info(f"Using STRUCTKIT_STRUCTURES_PATH: {args.structures_path}")
Expand Down
9 changes: 8 additions & 1 deletion structkit/commands/list.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from structkit.commands import Command
import os
import asyncio
from structkit.sources import SourceError, resolve_structures_path


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

def execute(self, args):
try:
args.structures_path, _ = resolve_structures_path(args.structures_path, getattr(args, 'source', None))
except SourceError as exc:
self.logger.error(f"❗ {exc}")
raise SystemExit(1) from exc
self.logger.info("Listing available structures")
if args.mcp:
self._list_structures_mcp(args)
Expand Down
84 changes: 84 additions & 0 deletions structkit/commands/sources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from structkit.commands import Command
from structkit.sources import (
SourceError,
add_source,
get_sources_config_path,
read_sources,
remove_source,
validate_source_path,
)


class SourcesCommand(Command):
"""Manage named custom structure sources."""

def __init__(self, parser):
super().__init__(parser)
parser.description = "Manage named custom structure sources"
parser.add_argument('--config-path', type=str, help='Override sources config path (env: STRUCTKIT_SOURCES_CONFIG)')
subparsers = parser.add_subparsers(dest='sources_command')

subparsers.add_parser('list', help='List configured sources').set_defaults(sources_func=self.list_sources)

add_parser = subparsers.add_parser('add', help='Add or update a local source')
add_parser.add_argument('name')
add_parser.add_argument('path_or_url')
add_parser.set_defaults(sources_func=self.add_source)

remove_parser = subparsers.add_parser('remove', help='Remove a configured source')
remove_parser.add_argument('name')
remove_parser.set_defaults(sources_func=self.remove_source)

show_parser = subparsers.add_parser('show', help='Show a configured source')
show_parser.add_argument('name')
show_parser.set_defaults(sources_func=self.show_source)

validate_parser = subparsers.add_parser('validate', help='Validate a configured source')
validate_parser.add_argument('name')
validate_parser.set_defaults(sources_func=self.validate_source)

parser.set_defaults(func=self.execute)

def execute(self, args):
if not hasattr(args, 'sources_func'):
self.parser.print_help()
return
try:
args.sources_func(args)
except SourceError as exc:
self.logger.error(f"❗ {exc}")
raise SystemExit(1) from exc

def list_sources(self, args):
sources = read_sources(args.config_path)
print(f"Sources config: {args.config_path or get_sources_config_path()}")
if not sources:
print("No sources configured.")
return
for name, path in sorted(sources.items()):
print(f"{name}\t{path}")

def add_source(self, args):
path = add_source(args.name, args.path_or_url, args.config_path)
print(f"Added source '{args.name}' -> {read_sources(args.config_path)[args.name]}")
print(f"Sources config: {path}")

def remove_source(self, args):
path = remove_source(args.name, args.config_path)
print(f"Removed source '{args.name}'")
print(f"Sources config: {path}")

def show_source(self, args):
sources = read_sources(args.config_path)
if args.name not in sources:
raise SourceError(f"source not found: {args.name}")
print(f"{args.name}\t{sources[args.name]}")

def validate_source(self, args):
sources = read_sources(args.config_path)
if args.name not in sources:
raise SourceError(f"source not found: {args.name}")
ok, message = validate_source_path(sources[args.name])
if not ok:
raise SourceError(message)
print(f"Source '{args.name}' is valid: {message}")
2 changes: 2 additions & 0 deletions structkit/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from structkit.commands.graph import GraphCommand
from structkit.commands.generate_schema import GenerateSchemaCommand
from structkit.commands.mcp import MCPCommand
from structkit.commands.sources import SourcesCommand
from structkit.logging_config import configure_logging

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

# init to create a basic .struct.yaml
from structkit.commands.init import InitCommand
Expand Down
Loading
Loading