From 5655e8f2f8a79c4ef6510ebc918ea9e005fa9b22 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 17:03:20 +0000 Subject: [PATCH] Rename default project file from .struct.yaml to .structkit.yaml The canonical project structure file is now .structkit.yaml. generate, init, and the reusable GitHub Action still accept .struct.yaml so existing repositories keep working. Co-authored-by: Kenneth Belitzky --- .github/instructions/struct.instructions.md | 4 +- .github/prompts/struct.prompt.md | 4 +- .github/workflows/struct-generate.yaml | 19 +- .struct.yaml => .structkit.yaml | 0 .vscode/settings.json | 7 +- README.md | 2 +- docker-compose.yaml | 4 +- docs/agent-skills.md | 4 +- docs/cli-reference.md | 18 +- docs/configuration.md | 6 +- docs/custom-structures.md | 10 +- docs/examples/index.md | 12 +- docs/github-integration.md | 2 + docs/hooks.md | 12 +- docs/installation.md | 2 +- docs/mcp-integration.md | 2 +- docs/quickstart.md | 10 +- docs/schema.md | 13 +- docs/usage.md | 12 +- docs/vhs/README.md | 2 +- docs/vhs/schema-generation.tape | 4 +- examples/README.md | 18 +- .../{.struct.yaml => .structkit.yaml} | 6 +- examples/ai-agent-mcp/README.md | 6 +- examples/config-layering/README.md | 4 +- .../project-config-example.yaml | 2 +- .../{.struct.yaml => .structkit.yaml} | 0 examples/fastapi-service/README.md | 2 +- .../{.struct.yaml => .structkit.yaml} | 0 examples/github-actions-ci/README.md | 2 +- .../{.struct.yaml => .structkit.yaml} | 0 examples/python-cli/README.md | 4 +- .../{.struct.yaml => .structkit.yaml} | 0 examples/terraform-module/README.md | 2 +- .../{.struct.yaml => .structkit.yaml} | 0 examples/versioned-sources/README.md | 8 +- structkit/commands/generate.py | 19 +- structkit/commands/init.py | 27 ++- structkit/config.py | 2 +- structkit/contribs/README.md | 4 +- structkit/contribs/github/prompts/struct.yaml | 6 +- .../contribs/github/workflows/run-struct.yaml | 6 +- structkit/main.py | 4 +- structkit/project_file.py | 113 ++++++++++++ tests/test_init_command.py | 11 +- tests/test_project_file.py | 166 ++++++++++++++++++ tests/test_sources_command.py | 4 +- 47 files changed, 449 insertions(+), 116 deletions(-) rename .struct.yaml => .structkit.yaml (100%) rename examples/ai-agent-mcp/{.struct.yaml => .structkit.yaml} (95%) rename examples/fastapi-service/{.struct.yaml => .structkit.yaml} (100%) rename examples/github-actions-ci/{.struct.yaml => .structkit.yaml} (100%) rename examples/python-cli/{.struct.yaml => .structkit.yaml} (100%) rename examples/terraform-module/{.struct.yaml => .structkit.yaml} (100%) rename examples/versioned-sources/{.struct.yaml => .structkit.yaml} (100%) create mode 100644 structkit/project_file.py create mode 100644 tests/test_project_file.py diff --git a/.github/instructions/struct.instructions.md b/.github/instructions/struct.instructions.md index 673e135..c92a05c 100644 --- a/.github/instructions/struct.instructions.md +++ b/.github/instructions/struct.instructions.md @@ -6,9 +6,9 @@ applyTo: '**' ## Role -You are an expert assistant that generates valid `.struct.yaml` files for the [StructKit tool](https://github.com/httpdss/structkit), which automates project structure generation from YAML configuration. +You are an expert assistant that generates valid `.structkit.yaml` files for the [StructKit tool](https://github.com/httpdss/structkit), which automates project structure generation from YAML configuration. -## Defining the `.struct.yaml` file +## Defining the `.structkit.yaml` file this file has 3 main keys: `structures`, `folders`, and `variables`. diff --git a/.github/prompts/struct.prompt.md b/.github/prompts/struct.prompt.md index 941418f..dbb175f 100644 --- a/.github/prompts/struct.prompt.md +++ b/.github/prompts/struct.prompt.md @@ -2,9 +2,9 @@ ## Role -You are an expert assistant that generates valid `.struct.yaml` files for the [StructKit tool](https://github.com/httpdss/structkit), which automates project structure generation from YAML configuration. +You are an expert assistant that generates valid `.structkit.yaml` files for the [StructKit tool](https://github.com/httpdss/structkit), which automates project structure generation from YAML configuration. -## Defining the `.struct.yaml` file +## Defining the `.structkit.yaml` file this file has 3 main keys: `structures`, `folders`, and `variables`. diff --git a/.github/workflows/struct-generate.yaml b/.github/workflows/struct-generate.yaml index 86b55ce..1e7e00d 100644 --- a/.github/workflows/struct-generate.yaml +++ b/.github/workflows/struct-generate.yaml @@ -4,10 +4,10 @@ on: workflow_call: inputs: struct_file: - description: 'Path to the StructKit file' + description: 'Path to the StructKit file. When omitted, uses .structkit.yaml, falling back to .struct.yaml.' type: string required: false - default: '.struct.yaml' + default: '' output_dir: description: 'Path to the output directory' type: string @@ -101,6 +101,17 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.token }} run: | + STRUCT_FILE='${{ inputs.struct_file }}' + if [ -z "$STRUCT_FILE" ]; then + if [ -f .structkit.yaml ]; then + STRUCT_FILE=.structkit.yaml + elif [ -f .struct.yaml ]; then + STRUCT_FILE=.struct.yaml + else + STRUCT_FILE=.structkit.yaml + fi + fi + if [ -d ./custom-structures ]; then structkit list \ @@ -110,7 +121,7 @@ jobs: --non-interactive \ -s ./custom-structures/${{ inputs.custom_structure_path }} \ ${{ inputs.args }} \ - ${{ inputs.struct_file }} \ + "$STRUCT_FILE" \ ${{ inputs.output_dir }} else structkit list @@ -118,7 +129,7 @@ jobs: structkit generate \ --non-interactive \ ${{ inputs.args }} \ - ${{ inputs.struct_file }} \ + "$STRUCT_FILE" \ ${{ inputs.output_dir }} fi diff --git a/.struct.yaml b/.structkit.yaml similarity index 100% rename from .struct.yaml rename to .structkit.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json index 80ba861..05afb0d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,11 @@ { "yaml.schemas": { - "./.vscode/struct-plugins.schema.json": "*.struct.yaml" + "./.vscode/struct-plugins.schema.json": [ + ".structkit.yaml", + ".struct.yaml", + "*.structkit.yaml", + "*.struct.yaml" + ] }, "python.languageServer": "None" } diff --git a/README.md b/README.md index 0432cb4..e43c575 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ docker run --rm -v "$(pwd):/workdir" ghcr.io/httpdss/structkit:main \ These are install paths for [StructKit](https://github.com/httpdss/structkit), not separate products. Star this repo. - [GitHub Action](https://github.com/httpdss/structkit-action) — validate, generate, or drift-check in CI -- [VS Code / Cursor](https://github.com/httpdss/vscode-structkit) — schema and autocomplete for `.struct.yaml` +- [VS Code / Cursor](https://github.com/httpdss/vscode-structkit) — schema and autocomplete for `.structkit.yaml` - [Agent skills](https://github.com/httpdss/structkit-skills) — inspect, preview, generate, validate from an assistant - [pre-commit](https://github.com/httpdss/structkit-pre-commit) — validate structures before they land - [Homebrew](https://github.com/httpdss/homebrew-structkit) — `brew tap httpdss/structkit && brew install structkit` diff --git a/docker-compose.yaml b/docker-compose.yaml index 1384c38..e5101bb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,7 +14,7 @@ services: "--backup=/app/backup", "--file-strategy=rename", "--log-file=/app/logfile.log", - "/app/examples/python-cli/.struct.yaml", + "/app/examples/python-cli/.structkit.yaml", "/app/example_project" ] structkit-test: @@ -31,6 +31,6 @@ services: "--backup=/app/backup", "--file-strategy=rename", "--log-file=/app/logfile.log", - "/app/examples/python-cli/.struct.yaml", + "/app/examples/python-cli/.structkit.yaml", "/app/example_project" ] diff --git a/docs/agent-skills.md b/docs/agent-skills.md index fe11d16..5ad2192 100644 --- a/docs/agent-skills.md +++ b/docs/agent-skills.md @@ -10,7 +10,7 @@ The [`structkit-workflows`](https://github.com/httpdss/structkit-skills) skill c 2. Inspect the selected structure and its variables. 3. Preview or dry-run the generation before writing files. 4. Use conservative file-conflict behavior for existing repositories. -5. Validate `.struct.yaml` files and generated output. +5. Validate `.structkit.yaml` files and generated output. This is useful when you want an assistant to scaffold from approved StructKit templates instead of inventing project structure from scratch. @@ -33,7 +33,7 @@ hermes skills install https://raw.githubusercontent.com/httpdss/structkit-skills Use the skill when an agent needs to: - Generate a project, Terraform module, CI baseline, documentation bundle, or application scaffold with StructKit. -- Author or update reusable `.struct.yaml` structures. +- Author or update reusable `.structkit.yaml` structures. - Review a StructKit generation plan before files are written. - Package StructKit-backed workflows for repeatable use across repositories. diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 8affb42..605d250 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -108,10 +108,10 @@ structkit lint [-h] [-l LOG] [-c CONFIG_FILE] [-i LOG_FILE] [-s STRUCTURES_PATH] Examples: ```sh -structkit lint .struct.yaml +structkit lint .structkit.yaml structkit lint structkit/contribs/project/python.yaml structkit lint --all -structkit lint .struct.yaml --json +structkit lint .structkit.yaml --json ``` ### `generate` @@ -125,7 +125,7 @@ structkit generate [-h] [-l LOG] [-c CONFIG_FILE] [-i LOG_FILE] [-s STRUCTURES_P ``` Defaults when omitted: -- structure_definition -> .struct.yaml +- structure_definition -> `.structkit.yaml` (falls back to `.struct.yaml` if the canonical file is missing) - base_path -> . Example: @@ -135,7 +135,7 @@ structkit generate **Arguments:** -- `structure_definition` (optional): Path to the YAML configuration file (default: `.struct.yaml`). +- `structure_definition` (optional): Path to the YAML configuration file (default: `.structkit.yaml`, with a fallback to `.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. Takes precedence over named sources. - `--source SOURCE`: Named source to use when resolving structure definitions. You can also use `/` as the structure definition. @@ -333,7 +333,7 @@ The `config print` command shows the final merged configuration from all layers: 1. Built-in defaults (lowest priority) 2. User config (`~/.config/struct/config.yaml`) -3. Project config (`.struct.yaml` or `--config-file`) +3. Project config (`.structkit.yaml`, legacy `.struct.yaml`, or `--config-file`) 4. CLI arguments (highest priority) The command also displays which configuration sources were loaded. @@ -364,7 +364,7 @@ See the [Configuration](configuration.md) documentation for more details on conf ### `init` -Initialize a basic .struct.yaml in the target directory. +Initialize a basic .structkit.yaml in the target directory. Usage: @@ -372,18 +372,18 @@ Usage: structkit init [path] ``` -- Creates a .struct.yaml if it does not exist. +- Creates a .structkit.yaml if it does not exist. - Includes: - pre_hooks/post_hooks with echo commands - files with a README.md placeholder - folders referencing github/workflows/run-structkit at ./ -- Non-destructive: if .struct.yaml already exists, it is not overwritten and a message is printed. +- Non-destructive: if `.structkit.yaml` or a legacy `.struct.yaml` already exists, it is not overwritten and a message is printed. ## Examples ### Using Defaults -Generate with default structure (.struct.yaml) into current directory: +Generate with default structure (.structkit.yaml) into current directory: ```sh structkit generate diff --git a/docs/configuration.md b/docs/configuration.md index f2eded2..873964c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -6,7 +6,7 @@ Structkit supports a layered configuration system that allows you to set default 1. **Built-in defaults** - Hard-coded defaults that are always present 2. **User config** - Global defaults from `~/.config/struct/config.yaml` -3. **Project config** - Project-specific config from `.struct.yaml` or `--config-file` +3. **Project config** - Project-specific config from `.structkit.yaml` (legacy `.struct.yaml`) or `--config-file` 4. **CLI arguments** - Command-line flags (highest priority) ### User Config @@ -24,7 +24,7 @@ log: WARNING ### Project Config -Project-specific settings can be defined in a `.struct.yaml` file or specified via the `--config-file` flag. These settings override user config and built-in defaults. +Project-specific settings can be defined in a `.structkit.yaml` file or specified via the `--config-file` flag. These settings override user config and built-in defaults. A legacy `.struct.yaml` file is still accepted when `.structkit.yaml` is not present. ### CLI Arguments @@ -80,7 +80,7 @@ The command also displays which configuration sources were used: Configuration sources: 1. Built-in defaults: always loaded 2. User config: /home/user/.config/struct/config.yaml (exists) - 3. Project config: .struct.yaml + 3. Project config: .structkit.yaml 4. CLI arguments: highest priority ``` diff --git a/docs/custom-structures.md b/docs/custom-structures.md index 92147c1..71820c0 100644 --- a/docs/custom-structures.md +++ b/docs/custom-structures.md @@ -2,7 +2,7 @@ Let's say you are happy with the default structures that StructKit provides, but you want to customize them for your specific needs. This is totally possible! -The best way to approach this is to have a repository where you can store your custom structures. You can then reference these structures in your `.struct.yaml` files. +The best way to approach this is to have a repository where you can store your custom structures. You can then reference these structures in your `.structkit.yaml` files. ## Suggested Repository Structure @@ -18,7 +18,7 @@ structures/ │ └── structure2.yaml ``` -This way you could reference your custom structures in your `.struct.yaml` files like this: +This way you could reference your custom structures in your `.structkit.yaml` files like this: ```yaml folders: @@ -33,7 +33,7 @@ folders: For this to work, you will need to set the path to the custom structures repository using the `-s` option when running StructKit: ```sh -structkit generate -s ~/path/to/custom-structures/structures file://.struct.yaml ./output +structkit generate -s ~/path/to/custom-structures/structures file://.structkit.yaml ./output ``` ## Named custom sources @@ -64,7 +64,7 @@ Git-backed sources are cloned into `$XDG_CACHE_HOME/structkit/sources` or `~/.ca ## File-local sources -Committed `.struct.yaml` files can declare their own named sources. This keeps generation portable and protects existing files from changes to a user's global `structkit sources` configuration. +Committed `.structkit.yaml` files can declare their own named sources. This keeps generation portable and protects existing files from changes to a user's global `structkit sources` configuration. Legacy `.struct.yaml` files are still read when `.structkit.yaml` is not present. ```yaml sources: @@ -104,7 +104,7 @@ folders: struct: github://httpdss/platform-structures@v1.2.0/structures/python/service ``` -For reproducible `.struct.yaml` files, prefer tags or commit SHAs over mutable branches. +For reproducible `.structkit.yaml` files, prefer tags or commit SHAs over mutable branches. Use a source explicitly with `--source`: diff --git a/docs/examples/index.md b/docs/examples/index.md index 42351c9..1bf657a 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -6,11 +6,11 @@ StructKit's polished examples live in the repository-level [`examples/`](../../e | Example | Use case | Source | |---|---|---| -| Python CLI | Small Python command-line tool with tests and package metadata | [`examples/python-cli/.struct.yaml`](../../examples/python-cli/.struct.yaml) | -| FastAPI service | API service with tests, Dockerfile, and environment example | [`examples/fastapi-service/.struct.yaml`](../../examples/fastapi-service/.struct.yaml) | -| Terraform module | Reusable infrastructure module skeleton | [`examples/terraform-module/.struct.yaml`](../../examples/terraform-module/.struct.yaml) | -| GitHub Actions CI baseline | Standard Python CI workflow and pre-commit setup | [`examples/github-actions-ci/.struct.yaml`](../../examples/github-actions-ci/.struct.yaml) | -| AI agent / MCP scaffold | Agent-friendly repository structure and instructions | [`examples/ai-agent-mcp/.struct.yaml`](../../examples/ai-agent-mcp/.struct.yaml) | +| Python CLI | Small Python command-line tool with tests and package metadata | [`examples/python-cli/.structkit.yaml`](../../examples/python-cli/.structkit.yaml) | +| FastAPI service | API service with tests, Dockerfile, and environment example | [`examples/fastapi-service/.structkit.yaml`](../../examples/fastapi-service/.structkit.yaml) | +| Terraform module | Reusable infrastructure module skeleton | [`examples/terraform-module/.structkit.yaml`](../../examples/terraform-module/.structkit.yaml) | +| GitHub Actions CI baseline | Standard Python CI workflow and pre-commit setup | [`examples/github-actions-ci/.structkit.yaml`](../../examples/github-actions-ci/.structkit.yaml) | +| AI agent / MCP scaffold | Agent-friendly repository structure and instructions | [`examples/ai-agent-mcp/.structkit.yaml`](../../examples/ai-agent-mcp/.structkit.yaml) | Start with the [examples index](../../examples/README.md) for copy-paste commands, expected output, and customization notes. @@ -19,7 +19,7 @@ Start with the [examples index](../../examples/README.md) for copy-paste command Every example can be previewed with `--dry-run --diff`: ```bash -structkit generate --vars "project_name=DemoCLI,package_name=demo_cli,cli_command=demo-cli,description=A small Python CLI generated by StructKit." examples/python-cli/.struct.yaml /tmp/structkit-python-cli-demo --dry-run --diff +structkit generate --vars "project_name=DemoCLI,package_name=demo_cli,cli_command=demo-cli,description=A small Python CLI generated by StructKit." examples/python-cli/.structkit.yaml /tmp/structkit-python-cli-demo --dry-run --diff ``` Then remove `--dry-run --diff` when you're ready to write files. diff --git a/docs/github-integration.md b/docs/github-integration.md index 64d5e49..089f955 100644 --- a/docs/github-integration.md +++ b/docs/github-integration.md @@ -21,6 +21,8 @@ jobs: token: ${{ secrets.STRUCT_RUN_TOKEN }} ``` +When `struct_file` is omitted, the workflow uses `.structkit.yaml` if present and falls back to a legacy `.struct.yaml`. + ## Best Practices 1. **Secure Your Token**: Store GitHub tokens in secrets management tools. diff --git a/docs/hooks.md b/docs/hooks.md index 0c2ebc3..0621ccf 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -187,11 +187,11 @@ You can disable hooks entirely using the `--no-hooks` flag or environment variab ```bash # Using CLI flag -structkit generate .struct.yaml --no-hooks +structkit generate .structkit.yaml --no-hooks # Using environment variable export STRUCTKIT_NO_HOOKS=true -structkit generate .struct.yaml +structkit generate .structkit.yaml ``` This is recommended for: @@ -204,7 +204,7 @@ This is recommended for: By default, StructKit prompts for confirmation before running hooks in interactive mode: ```bash -$ structkit generate .struct.yaml +$ structkit generate .structkit.yaml ⚠️ The following pre-hooks will be executed: - echo "Preparing environment..." @@ -243,11 +243,11 @@ When an allowlist exists: You can also specify a custom allowlist path: ```bash -structkit generate .struct.yaml --hooks-allowlist /path/to/allowlist.txt +structkit generate .structkit.yaml --hooks-allowlist /path/to/allowlist.txt # Or via environment variable export STRUCTKIT_HOOKS_ALLOWLIST=/path/to/allowlist.txt -structkit generate .struct.yaml +structkit generate .structkit.yaml ``` ### MCP Integration Safety @@ -301,7 +301,7 @@ post_hooks: ### Safe Hook Example with Allowlist ```yaml -# .struct.yaml +# .structkit.yaml pre_hooks: - echo "Preparing environment..." - python -c "import sys; print(sys.version)" diff --git a/docs/installation.md b/docs/installation.md index 1ce3011..81aa831 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -25,7 +25,7 @@ Clone the repository and install locally. See the [Development](development.md) Run StructKit without installing, using Docker: ```sh -docker run -v $(pwd):/workdir -u $(id -u):$(id -g) ghcr.io/httpdss/structkit:main generate file:///workdir/examples/python-cli/.struct.yaml /workdir/example_output +docker run -v $(pwd):/workdir -u $(id -u):$(id -g) ghcr.io/httpdss/structkit:main generate file:///workdir/examples/python-cli/.structkit.yaml /workdir/example_output ``` Refer to the [Quick Start](quickstart.md) guide for more options. diff --git a/docs/mcp-integration.md b/docs/mcp-integration.md index 2f845de..186ebd1 100644 --- a/docs/mcp-integration.md +++ b/docs/mcp-integration.md @@ -113,7 +113,7 @@ Lint one or more structure YAML files or structure names for quality and safety { "name": "lint_structure", "arguments": { - "targets": ["project/python", "/path/to/.struct.yaml"], + "targets": ["project/python", "/path/to/.structkit.yaml"], "structures_path": "/path/to/custom/structures", "lint_all": false, "output": "json" diff --git a/docs/quickstart.md b/docs/quickstart.md index fe9198e..f03c9d2 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -17,7 +17,7 @@ docker run \ -v $(pwd):/workdir \ -u $(id -u):$(id -g) \ ghcr.io/httpdss/structkit:main generate \ - file:///workdir/examples/python-cli/.struct.yaml \ + file:///workdir/examples/python-cli/.structkit.yaml \ /workdir/example_output ``` @@ -28,7 +28,7 @@ docker run \ -v $(pwd):/workdir \ -u $(id -u):$(id -g) \ ghcr.io/httpdss/structkit:alpine generate \ - file:///workdir/examples/python-cli/.struct.yaml \ + file:///workdir/examples/python-cli/.structkit.yaml \ /workdir/example_output ``` @@ -50,7 +50,7 @@ vim structure.yaml # or copy one of the examples from the examples/ directory structkit generate structure.yaml . ``` -> Note: The `file://` protocol is automatically added for `.yaml` files, so `structure.yaml` and `file://structure.yaml` work identically. Additionally, if your file is named `.struct.yaml` in the current directory and you want to generate into the current directory, you can just run `structkit generate`. +> Note: The `file://` protocol is automatically added for `.yaml` files, so `structure.yaml` and `file://structure.yaml` work identically. Additionally, if your file is named `.structkit.yaml` in the current directory (or the legacy `.struct.yaml`) and you want to generate into the current directory, you can just run `structkit generate`. ## Discovering Available Structures @@ -83,13 +83,13 @@ structkit generate project/nodejs ./my-node-app ## Bootstrap a new project -Start with a minimal .struct.yaml: +Start with a minimal .structkit.yaml: ```sh structkit init ``` -This writes a basic .struct.yaml with hooks, a README, and a reference to the run-structkit workflow. +This writes a basic .structkit.yaml with hooks, a README, and a reference to the run-structkit workflow. ## Next Steps diff --git a/docs/schema.md b/docs/schema.md index 3d1a24e..7631cfb 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -20,19 +20,24 @@ https://raw.githubusercontent.com/httpdss/structkit/main/struct-schema.json ```json { "yaml.schemas": { - "https://raw.githubusercontent.com/httpdss/structkit/main/struct-schema.json": ".struct.yaml" + "https://raw.githubusercontent.com/httpdss/structkit/main/struct-schema.json": [ + ".structkit.yaml", + ".struct.yaml", + "*.structkit.yaml", + "*.struct.yaml" + ] } } ``` -This provides validation and autocompletion for all `.struct.yaml` files. +This provides validation and autocompletion for `.structkit.yaml` files. Legacy `.struct.yaml` files are included for compatibility. ### JetBrains IDEs (IntelliJ, PyCharm, etc.) 1. Go to **Settings** → **Languages & Frameworks** → **Schemas and DTDs** → **JSON Schema Mappings** 2. Click **+** to add a new mapping 3. Set **Schema file or URL** to: `https://raw.githubusercontent.com/httpdss/structkit/main/struct-schema.json` -4. Set **File path pattern** to: `*.struct.yaml` +4. Set **File path pattern** to: `*.structkit.yaml` (also add `*.struct.yaml` if you still use the legacy name) ## Generating Custom Schema @@ -45,7 +50,7 @@ structkit generate-schema -s /path/to/custom/structures -o my-schema.json # Use in VS Code settings { "yaml.schemas": { - "./my-schema.json": ".struct.yaml" + "./my-schema.json": [".structkit.yaml", ".struct.yaml"] } } ``` diff --git a/docs/usage.md b/docs/usage.md index 635e2cf..e018e57 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -36,12 +36,14 @@ structkit generate ### Using Defaults -If you have a .struct.yaml in the current directory and want to generate into the current directory, you can simply run: +If you have a `.structkit.yaml` in the current directory and want to generate into the current directory, you can simply run: ```sh structkit generate ``` +The canonical project file is `.structkit.yaml`. If that file is missing, `structkit generate` still reads a legacy `.struct.yaml` in the same directory. If both exist, `.structkit.yaml` is used. + ### Simple Example ```sh @@ -58,7 +60,7 @@ structkit generate my-config.yaml ./output structkit generate file://my-config.yaml ./output ``` -Tip: If your config file is named `.struct.yaml` in the current directory and you want to generate into the current directory, you can simply run: +Tip: If your config file is named `.structkit.yaml` (or legacy `.struct.yaml`) in the current directory and you want to generate into the current directory, you can simply run: ```sh structkit generate @@ -120,15 +122,15 @@ structkit generate-schema -s /path/to/custom/structures -o schema.json The generated schema includes all available structures from both the built-in contribs directory and any custom structures path you specify. This is useful for: -- IDE autocompletion when writing `.struct.yaml` files +- IDE autocompletion when writing `.structkit.yaml` files - Validation of structure references in your configurations - Programmatic discovery of available templates ## Other Commands -### Initialize a project with .struct.yaml +### Initialize a project with .structkit.yaml -Create a minimal .struct.yaml in the current directory: +Create a minimal .structkit.yaml in the current directory: ```sh structkit init diff --git a/docs/vhs/README.md b/docs/vhs/README.md index 1e5864e..41a32bd 100644 --- a/docs/vhs/README.md +++ b/docs/vhs/README.md @@ -355,7 +355,7 @@ Type "echo && echo 'Available structures in schema:' && jq -r '.definitions.Plug Sleep 5 Type "" Enter Type "# Schema can be used in VS Code for autocompletion" Enter -Type "echo 'Add to your .struct.yaml files for IDE support:'" Enter +Type "echo 'Add to your .structkit.yaml files for IDE support:'" Enter Type "echo '# yaml-language-server: \$schema=./struct-schema.json'" Enter Sleep 3 ``` diff --git a/docs/vhs/schema-generation.tape b/docs/vhs/schema-generation.tape index 127f2a4..2f15b4a 100644 --- a/docs/vhs/schema-generation.tape +++ b/docs/vhs/schema-generation.tape @@ -44,7 +44,7 @@ Type "{" Enter Type ' "yaml.schemas": {' Enter -Type ' "./.vscode/struct-plugins.schema.json": "*.struct.yaml"' +Type ' "./.vscode/struct-plugins.schema.json": "*.structkit.yaml"' Enter Type " }" Enter @@ -56,7 +56,7 @@ Sleep 2s Type "# Now VSCode will provide autocompletion for structkit names!" Enter -Type "# when you type 'struct:' in a .struct.yaml file" +Type "# when you type 'struct:' in a .structkit.yaml file" Enter Sleep 2s diff --git a/examples/README.md b/examples/README.md index 33b92df..d59f588 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,19 +6,19 @@ This directory contains polished, copy-pasteable StructKit examples for common d | Example | Best for | Command | |---|---|---| -| [Python CLI](python-cli/) | Packaging a small Python command-line tool | `structkit generate --vars "project_name=DemoCLI,package_name=demo_cli,cli_command=demo-cli,description=A small Python CLI generated by StructKit." examples/python-cli/.struct.yaml ./demo-python-cli` | -| [FastAPI service](fastapi-service/) | Bootstrapping an API service with tests and Docker | `structkit generate --vars "service_name=DemoAPI,module_name=app,description=A small FastAPI service generated by StructKit." examples/fastapi-service/.struct.yaml ./demo-fastapi-service` | -| [Terraform module](terraform-module/) | Creating a reusable infrastructure module skeleton | `structkit generate --vars "module_name=terraform-example-module,description=A reusable Terraform module generated by StructKit.,terraform_version=1.6.0,provider_source=hashicorp/null,provider_version=>= 3.2.0" examples/terraform-module/.struct.yaml ./demo-terraform-module` | -| [GitHub Actions CI baseline](github-actions-ci/) | Adding a standard Python CI workflow to a repo | `structkit generate --vars "project_name=DemoPythonProject,python_version=3.12" examples/github-actions-ci/.struct.yaml ./demo-ci-baseline` | -| [AI agent / MCP scaffold](ai-agent-mcp/) | Giving AI coding assistants a project structure and instructions | `structkit generate --vars "project_name=AI Assisted Project,description=A project scaffolded for AI-assisted development with StructKit." examples/ai-agent-mcp/.struct.yaml ./demo-ai-agent-project` | -| [Versioned sources](versioned-sources/) | Pinning reusable struct sources and inheriting the same source in nested structs | `structkit generate examples/versioned-sources/.struct.yaml ./demo-versioned-sources` | +| [Python CLI](python-cli/) | Packaging a small Python command-line tool | `structkit generate --vars "project_name=DemoCLI,package_name=demo_cli,cli_command=demo-cli,description=A small Python CLI generated by StructKit." examples/python-cli/.structkit.yaml ./demo-python-cli` | +| [FastAPI service](fastapi-service/) | Bootstrapping an API service with tests and Docker | `structkit generate --vars "service_name=DemoAPI,module_name=app,description=A small FastAPI service generated by StructKit." examples/fastapi-service/.structkit.yaml ./demo-fastapi-service` | +| [Terraform module](terraform-module/) | Creating a reusable infrastructure module skeleton | `structkit generate --vars "module_name=terraform-example-module,description=A reusable Terraform module generated by StructKit.,terraform_version=1.6.0,provider_source=hashicorp/null,provider_version=>= 3.2.0" examples/terraform-module/.structkit.yaml ./demo-terraform-module` | +| [GitHub Actions CI baseline](github-actions-ci/) | Adding a standard Python CI workflow to a repo | `structkit generate --vars "project_name=DemoPythonProject,python_version=3.12" examples/github-actions-ci/.structkit.yaml ./demo-ci-baseline` | +| [AI agent / MCP scaffold](ai-agent-mcp/) | Giving AI coding assistants a project structure and instructions | `structkit generate --vars "project_name=AI Assisted Project,description=A project scaffolded for AI-assisted development with StructKit." examples/ai-agent-mcp/.structkit.yaml ./demo-ai-agent-project` | +| [Versioned sources](versioned-sources/) | Pinning reusable struct sources and inheriting the same source in nested structs | `structkit generate examples/versioned-sources/.structkit.yaml ./demo-versioned-sources` | ## How to run an example From the repository root: ```bash -structkit generate --vars "project_name=DemoCLI,package_name=demo_cli,cli_command=demo-cli,description=A small Python CLI generated by StructKit." examples/python-cli/.struct.yaml /tmp/structkit-python-cli-demo +structkit generate --vars "project_name=DemoCLI,package_name=demo_cli,cli_command=demo-cli,description=A small Python CLI generated by StructKit." examples/python-cli/.structkit.yaml /tmp/structkit-python-cli-demo ``` Most examples include defaults for variables. Pass `--vars` when you want a non-interactive, copy-pasteable command with a specific project name or package name: @@ -26,14 +26,14 @@ Most examples include defaults for variables. Pass `--vars` when you want a non- ```bash structkit generate \ --vars "project_name=DemoCLI,package_name=demo_cli,cli_command=demo-cli,description=A small Python CLI generated by StructKit." \ - examples/python-cli/.struct.yaml \ + examples/python-cli/.structkit.yaml \ /tmp/demo-cli ``` Use `--dry-run --diff` to preview output before writing files: ```bash -structkit generate --vars "service_name=DemoAPI,module_name=app,description=A small FastAPI service generated by StructKit." examples/fastapi-service/.struct.yaml /tmp/demo-api --dry-run --diff +structkit generate --vars "service_name=DemoAPI,module_name=app,description=A small FastAPI service generated by StructKit." examples/fastapi-service/.structkit.yaml /tmp/demo-api --dry-run --diff ``` ## Notes for contributors diff --git a/examples/ai-agent-mcp/.struct.yaml b/examples/ai-agent-mcp/.structkit.yaml similarity index 95% rename from examples/ai-agent-mcp/.struct.yaml rename to examples/ai-agent-mcp/.structkit.yaml index ede5e3b..05e5cb8 100644 --- a/examples/ai-agent-mcp/.struct.yaml +++ b/examples/ai-agent-mcp/.structkit.yaml @@ -25,9 +25,9 @@ files: ```bash # Replace with project-specific checks - structkit validate .struct.yaml + structkit validate .structkit.yaml ``` - - .struct.yaml: + - .structkit.yaml: content: | files: - README.md: @@ -84,7 +84,7 @@ files: - [ ] Replace generated placeholders with project-specific content. - [ ] Add canonical build and test commands to `AGENTS.md`. - - [ ] Validate `.struct.yaml` after customizing it. + - [ ] Validate `.structkit.yaml` after customizing it. variables: - project_name: description: "Project name." diff --git a/examples/ai-agent-mcp/README.md b/examples/ai-agent-mcp/README.md index da9e2aa..43a17f6 100644 --- a/examples/ai-agent-mcp/README.md +++ b/examples/ai-agent-mcp/README.md @@ -9,19 +9,19 @@ Use this when creating a repo that will be edited by AI coding assistants and yo ## Command ```bash -structkit generate --vars "project_name=AI Assisted Project,description=A project scaffolded for AI-assisted development with StructKit." examples/ai-agent-mcp/.struct.yaml ./demo-ai-agent-project +structkit generate --vars "project_name=AI Assisted Project,description=A project scaffolded for AI-assisted development with StructKit." examples/ai-agent-mcp/.structkit.yaml ./demo-ai-agent-project ``` ## Expected output - `README.md` - `AGENTS.md` -- `.struct.yaml` +- `.structkit.yaml` - `docs/architecture.md` - `docs/decisions/0001-record-architecture-decisions.md` - `tasks/backlog.md` ## Customization notes -- Commit the generated `.struct.yaml` so humans and AI agents can regenerate the agreed structure. +- Commit the generated `.structkit.yaml` so humans and AI agents can regenerate the agreed structure. - Adapt `AGENTS.md` with repository-specific build, test, and review commands. diff --git a/examples/config-layering/README.md b/examples/config-layering/README.md index 7737ac5..d584d08 100644 --- a/examples/config-layering/README.md +++ b/examples/config-layering/README.md @@ -8,7 +8,7 @@ Structkit supports configuration at four levels (from lowest to highest priority 1. **Built-in defaults** - Hard-coded baseline values 2. **User config** - Global defaults from `~/.config/struct/config.yaml` -3. **Project config** - Project-specific config from `.struct.yaml` or `--config-file` +3. **Project config** - Project-specific config from `.structkit.yaml` (legacy `.struct.yaml`) or `--config-file` 4. **CLI arguments** - Command-line flags (highest priority) ## Setup @@ -138,7 +138,7 @@ backup: ~/structkit-backups Share project-specific settings in version control: ```yaml -# .struct.yaml (checked into git) +# .structkit.yaml (checked into git) structures_path: ./team-structures input_store: ./.structkit/input.json non_interactive: true diff --git a/examples/config-layering/project-config-example.yaml b/examples/config-layering/project-config-example.yaml index d52cde2..0cdc744 100644 --- a/examples/config-layering/project-config-example.yaml +++ b/examples/config-layering/project-config-example.yaml @@ -1,7 +1,7 @@ # Example project config file # # This file can be checked into version control to share project-specific -# settings with your team. Use with --config-file or as .struct.yaml +# settings with your team. Use with --config-file or as .structkit.yaml # Project-specific structures location structures_path: ./custom-structures diff --git a/examples/fastapi-service/.struct.yaml b/examples/fastapi-service/.structkit.yaml similarity index 100% rename from examples/fastapi-service/.struct.yaml rename to examples/fastapi-service/.structkit.yaml diff --git a/examples/fastapi-service/README.md b/examples/fastapi-service/README.md index 208622f..1b00b75 100644 --- a/examples/fastapi-service/README.md +++ b/examples/fastapi-service/README.md @@ -9,7 +9,7 @@ Use this when starting a small internal API, service prototype, or baseline for ## Command ```bash -structkit generate --vars "service_name=DemoAPI,module_name=app,description=A small FastAPI service generated by StructKit." examples/fastapi-service/.struct.yaml ./demo-fastapi-service +structkit generate --vars "service_name=DemoAPI,module_name=app,description=A small FastAPI service generated by StructKit." examples/fastapi-service/.structkit.yaml ./demo-fastapi-service ``` ## Expected output diff --git a/examples/github-actions-ci/.struct.yaml b/examples/github-actions-ci/.structkit.yaml similarity index 100% rename from examples/github-actions-ci/.struct.yaml rename to examples/github-actions-ci/.structkit.yaml diff --git a/examples/github-actions-ci/README.md b/examples/github-actions-ci/README.md index cc4edfd..4a5e562 100644 --- a/examples/github-actions-ci/README.md +++ b/examples/github-actions-ci/README.md @@ -9,7 +9,7 @@ Use this when you want to apply a consistent CI baseline across internal Python ## Command ```bash -structkit generate --vars "project_name=DemoPythonProject,python_version=3.12" examples/github-actions-ci/.struct.yaml ./demo-ci-baseline +structkit generate --vars "project_name=DemoPythonProject,python_version=3.12" examples/github-actions-ci/.structkit.yaml ./demo-ci-baseline ``` ## Expected output diff --git a/examples/python-cli/.struct.yaml b/examples/python-cli/.structkit.yaml similarity index 100% rename from examples/python-cli/.struct.yaml rename to examples/python-cli/.structkit.yaml diff --git a/examples/python-cli/README.md b/examples/python-cli/README.md index 76c8965..bd99392 100644 --- a/examples/python-cli/README.md +++ b/examples/python-cli/README.md @@ -9,7 +9,7 @@ Use this when you want a quick starting point for an internal automation CLI, de ## Command ```bash -structkit generate --vars "project_name=DemoCLI,package_name=demo_cli,cli_command=demo-cli,description=A small Python CLI generated by StructKit." examples/python-cli/.struct.yaml ./demo-python-cli +structkit generate --vars "project_name=DemoCLI,package_name=demo_cli,cli_command=demo-cli,description=A small Python CLI generated by StructKit." examples/python-cli/.structkit.yaml ./demo-python-cli ``` Optional custom values: @@ -19,7 +19,7 @@ structkit generate \ --var project_name=ReleaseHelper \ --var package_name=release_helper \ --var cli_command=release-helper \ - examples/python-cli/.struct.yaml \ + examples/python-cli/.structkit.yaml \ ./release-helper ``` diff --git a/examples/terraform-module/.struct.yaml b/examples/terraform-module/.structkit.yaml similarity index 100% rename from examples/terraform-module/.struct.yaml rename to examples/terraform-module/.structkit.yaml diff --git a/examples/terraform-module/README.md b/examples/terraform-module/README.md index 0648ea5..4ad5e17 100644 --- a/examples/terraform-module/README.md +++ b/examples/terraform-module/README.md @@ -9,7 +9,7 @@ Use this when starting an infrastructure module that should have consistent vari ## Command ```bash -structkit generate --vars "module_name=terraform-example-module,description=A reusable Terraform module generated by StructKit.,terraform_version=1.6.0,provider_source=hashicorp/null,provider_version=>= 3.2.0" examples/terraform-module/.struct.yaml ./demo-terraform-module +structkit generate --vars "module_name=terraform-example-module,description=A reusable Terraform module generated by StructKit.,terraform_version=1.6.0,provider_source=hashicorp/null,provider_version=>= 3.2.0" examples/terraform-module/.structkit.yaml ./demo-terraform-module ``` ## Expected output diff --git a/examples/versioned-sources/.struct.yaml b/examples/versioned-sources/.structkit.yaml similarity index 100% rename from examples/versioned-sources/.struct.yaml rename to examples/versioned-sources/.structkit.yaml diff --git a/examples/versioned-sources/README.md b/examples/versioned-sources/README.md index 308bdf6..f9f76a7 100644 --- a/examples/versioned-sources/README.md +++ b/examples/versioned-sources/README.md @@ -1,6 +1,6 @@ # Versioned Sources Example -This example shows how a committed `.struct.yaml` can define its own named source and then reference structs from that source. +This example shows how a committed `.structkit.yaml` can define its own named source and then reference structs from that source. The example uses a local source so it works offline: @@ -31,14 +31,14 @@ sources: - `platform/app/base` resolves through the file-local `platform` source. - `app/base.yaml` references `app/ci` without a source prefix. - The nested `app/ci` reference inherits the same `platform` source context. -- A future change to the user's global `structkit sources` config will not affect this `.struct.yaml` file. +- A future change to the user's global `structkit sources` config will not affect this `.structkit.yaml` file. ## Run it From the StructKit repository root: ```bash -structkit generate examples/versioned-sources/.struct.yaml /tmp/structkit-versioned-sources-demo +structkit generate examples/versioned-sources/.structkit.yaml /tmp/structkit-versioned-sources-demo ``` Expected generated files: @@ -52,5 +52,5 @@ Expected generated files: Preview without writing files: ```bash -structkit generate examples/versioned-sources/.struct.yaml /tmp/structkit-versioned-sources-demo --dry-run --diff +structkit generate examples/versioned-sources/.structkit.yaml /tmp/structkit-versioned-sources-demo --dry-run --diff ``` diff --git a/structkit/commands/generate.py b/structkit/commands/generate.py index 16b3865..8961e38 100644 --- a/structkit/commands/generate.py +++ b/structkit/commands/generate.py @@ -8,6 +8,11 @@ from structkit.sources import SourceError, resolve_structures_path from structkit.struct_refs import SourceContext, resolve_struct_reference from structkit.input_store import InputStoreError +from structkit.project_file import ( + CANONICAL_PROJECT_STRUCT_FILE, + LEGACY_PROJECT_STRUCT_FILE, + resolve_project_struct_file, +) import subprocess @@ -21,7 +26,16 @@ class GenerateCommand(Command): def __init__(self, parser): super().__init__(parser) parser.description = "Generate the project structure from a YAML configuration file" - structure_arg = parser.add_argument('structure_definition', nargs='?', default='.struct.yaml', type=str, help='Path to the YAML configuration file (default: .struct.yaml)') + structure_arg = parser.add_argument( + 'structure_definition', + nargs='?', + default=CANONICAL_PROJECT_STRUCT_FILE, + type=str, + help=( + f'Path to the YAML configuration file (default: {CANONICAL_PROJECT_STRUCT_FILE}; ' + f'falls back to {LEGACY_PROJECT_STRUCT_FILE} for compatibility)' + ), + ) structure_arg.completer = structures_completer parser.add_argument('base_path', nargs='?', default='.', type=str, help='Base path where the structure will be created (default: current directory)') parser.add_argument( @@ -246,6 +260,9 @@ def _validate_loaded_config(self, config): return config def execute(self, args): + args.structure_definition = resolve_project_struct_file( + explicit_path=getattr(args, 'structure_definition', None), + ) try: args.structures_path, args.structure_definition = resolve_structures_path( args.structures_path, diff --git a/structkit/commands/init.py b/structkit/commands/init.py index 4cd89ff..73c7b31 100644 --- a/structkit/commands/init.py +++ b/structkit/commands/init.py @@ -1,6 +1,10 @@ from structkit.commands import Command import os import textwrap +from structkit.project_file import ( + CANONICAL_PROJECT_STRUCT_FILE, + LEGACY_PROJECT_STRUCT_FILE, +) BASIC_STRUCTKIT_YAML = textwrap.dedent( """ @@ -17,7 +21,7 @@ - README.md: | # Project - Initialized with struct. + Initialized with structkit. folders: - ./: @@ -30,24 +34,33 @@ class InitCommand(Command): def __init__(self, parser): super().__init__(parser) - parser.description = "Initialize a basic .struct.yaml in the target directory" + parser.description = f"Initialize a basic {CANONICAL_PROJECT_STRUCT_FILE} in the target directory" parser.add_argument('path', nargs='?', default='.', help='Directory to initialize (default: current directory)') parser.set_defaults(func=self.execute) def execute(self, args): base_path = os.path.abspath(args.path or '.') - target = os.path.join(base_path, '.struct.yaml') + target = os.path.join(base_path, CANONICAL_PROJECT_STRUCT_FILE) + legacy = os.path.join(base_path, LEGACY_PROJECT_STRUCT_FILE) os.makedirs(base_path, exist_ok=True) - # If file exists, do not overwrite without explicit confirmation behavior (keep simple: skip) + # If either canonical or legacy file exists, do not overwrite. if os.path.exists(target): - self.logger.info(f".struct.yaml already exists at {target}, skipping creation") - print(f"⚠️ .struct.yaml already exists at: {target}") + self.logger.info(f"{CANONICAL_PROJECT_STRUCT_FILE} already exists at {target}, skipping creation") + print(f"⚠️ {CANONICAL_PROJECT_STRUCT_FILE} already exists at: {target}") + return + + if os.path.exists(legacy): + self.logger.info( + f"{LEGACY_PROJECT_STRUCT_FILE} already exists at {legacy}, skipping creation" + ) + print(f"⚠️ {LEGACY_PROJECT_STRUCT_FILE} already exists at: {legacy}") + print(f" Consider renaming it to {CANONICAL_PROJECT_STRUCT_FILE}.") return with open(target, 'w') as f: f.write(BASIC_STRUCTKIT_YAML) - print("✅ Created .struct.yaml") + print(f"✅ Created {CANONICAL_PROJECT_STRUCT_FILE}") print(f" - {target}") diff --git a/structkit/config.py b/structkit/config.py index dbe38b9..a41afe4 100644 --- a/structkit/config.py +++ b/structkit/config.py @@ -3,7 +3,7 @@ Supports loading and merging configuration from multiple sources: 1. Built-in defaults 2. User config (~/.config/struct/config.yaml) -3. Project config (.struct.yaml or --config-file) +3. Project config (.structkit.yaml, legacy .struct.yaml, or --config-file) 4. CLI arguments Priority order: CLI args > Project config > User config > Built-in defaults diff --git a/structkit/contribs/README.md b/structkit/contribs/README.md index 19e984c..de7aa67 100644 --- a/structkit/contribs/README.md +++ b/structkit/contribs/README.md @@ -194,8 +194,8 @@ The `contribs` folder contains various sub-structs that can be used to generate #### `github/prompts/struct` -- **Description**: Provides a prompt for creating a new .struct.yaml file and workflow to run struct. -- **When to Use**: Use this sub-structkit when you need to create a new .struct.yaml file and workflow to run struct. +- **Description**: Provides a prompt for creating a new .structkit.yaml file and workflow to run struct. +- **When to Use**: Use this sub-structkit when you need to create a new .structkit.yaml file and workflow to run struct. ### Project diff --git a/structkit/contribs/github/prompts/struct.yaml b/structkit/contribs/github/prompts/struct.yaml index 28818bc..fb11b91 100644 --- a/structkit/contribs/github/prompts/struct.yaml +++ b/structkit/contribs/github/prompts/struct.yaml @@ -1,6 +1,6 @@ description: > Creates a GitHub Copilot prompt that configures an AI assistant for generating - valid .struct.yaml files for the StructKit tool, with schema references and usage examples. + valid .structkit.yaml files for the StructKit tool, with schema references and usage examples. files: - .github/prompts/struct.prompt.md: @@ -9,9 +9,9 @@ files: ## Role - You are an expert assistant that generates valid `.struct.yaml` files for the [StructKit tool](https://github.com/httpdss/struct), which automates project structure generation from YAML configuration. + You are an expert assistant that generates valid `.structkit.yaml` files for the [StructKit tool](https://github.com/httpdss/struct), which automates project structure generation from YAML configuration. - ## Defining the `.struct.yaml` file + ## Defining the `.structkit.yaml` file this file has 3 main keys: `structures`, `folders`, and `variables`. diff --git a/structkit/contribs/github/workflows/run-struct.yaml b/structkit/contribs/github/workflows/run-struct.yaml index 798b32d..90a5eac 100644 --- a/structkit/contribs/github/workflows/run-struct.yaml +++ b/structkit/contribs/github/workflows/run-struct.yaml @@ -1,6 +1,6 @@ description: > Creates a GitHub Actions workflow (run-struct.yaml) that can be manually triggered - via workflow_dispatch to run struct file generation, plus a starter .struct.yaml file. + via workflow_dispatch to run struct file generation, plus a starter .structkit.yaml file. files: - .github/workflows/run-struct.yaml: @@ -15,11 +15,11 @@ files: generate: uses: httpdss/structkit/.github/workflows/struct-generate.yaml@main with: - struct_file: 'file://.struct.yaml' + struct_file: 'file://.structkit.yaml' output_dir: '.' secrets: token: {{@ github_token @}} - - .struct.yaml: + - .structkit.yaml: skip_if_exists: true content: | files: diff --git a/structkit/main.py b/structkit/main.py index 12ca15d..99a9e69 100644 --- a/structkit/main.py +++ b/structkit/main.py @@ -141,9 +141,9 @@ def get_parser(): SourcesCommand(subparsers.add_parser('sources', help='Manage named custom structure sources')) ConfigCommand(subparsers.add_parser('config', help='Display and manage structkit configuration')) - # init to create a basic .struct.yaml + # init to create a basic .structkit.yaml from structkit.commands.init import InitCommand - InitCommand(subparsers.add_parser('init', help='Initialize a basic .struct.yaml in the target directory')) + InitCommand(subparsers.add_parser('init', help='Initialize a basic .structkit.yaml in the target directory')) # completion manager from structkit.commands.completion import CompletionCommand diff --git a/structkit/project_file.py b/structkit/project_file.py new file mode 100644 index 0000000..cde8b1b --- /dev/null +++ b/structkit/project_file.py @@ -0,0 +1,113 @@ +"""Canonical and legacy project structure filenames. + +The product was originally named STRUCT, so repositories commonly commit +``.struct.yaml``. The canonical name is now ``.structkit.yaml``. Commands that +default to the project structure file prefer the canonical name and fall back +to the legacy name when it is the only one present. +""" + +import logging +import os +from typing import Optional, Tuple + +logger = logging.getLogger(__name__) + +CANONICAL_PROJECT_STRUCT_FILE = ".structkit.yaml" +LEGACY_PROJECT_STRUCT_FILE = ".struct.yaml" +PROJECT_STRUCT_FILES = ( + CANONICAL_PROJECT_STRUCT_FILE, + LEGACY_PROJECT_STRUCT_FILE, +) + + +def strip_file_scheme(path: str) -> Tuple[str, bool]: + """Return (local_path, had_file_scheme) for a structure path.""" + if path.startswith("file://"): + return path[7:], True + return path, False + + +def is_default_project_struct_path(path: Optional[str]) -> bool: + """Return True when path is omitted or names a default project file.""" + if not path: + return True + local_path, _ = strip_file_scheme(path) + return os.path.basename(local_path) in PROJECT_STRUCT_FILES + + +def find_existing_project_struct_file(directory: str = ".") -> Optional[str]: + """Return the existing default project file in directory, if any. + + Prefers ``.structkit.yaml`` over the legacy ``.struct.yaml``. + """ + canonical = os.path.join(directory, CANONICAL_PROJECT_STRUCT_FILE) + if os.path.isfile(canonical): + return canonical + + legacy = os.path.join(directory, LEGACY_PROJECT_STRUCT_FILE) + if os.path.isfile(legacy): + return legacy + + return None + + +def resolve_project_struct_file( + explicit_path: Optional[str] = None, + directory: str = ".", +) -> str: + """Resolve which project structure file a command should use. + + Non-default paths (named structures, custom YAML files) are returned + unchanged. Default names prefer ``.structkit.yaml`` and fall back to + ``.struct.yaml`` when the canonical file is missing. + + If neither default file exists, the canonical name is returned so error + messages mention ``.structkit.yaml``. + """ + if explicit_path and not is_default_project_struct_path(explicit_path): + return explicit_path + + file_scheme = False + search_dir = directory + if explicit_path: + local_path, file_scheme = strip_file_scheme(explicit_path) + parent = os.path.dirname(local_path) + if parent: + search_dir = parent + existing_path = local_path + else: + existing_path = os.path.join(directory, os.path.basename(local_path)) + if os.path.isfile(existing_path): + return explicit_path + + canonical = os.path.normpath(os.path.join(search_dir, CANONICAL_PROJECT_STRUCT_FILE)) + legacy = os.path.normpath(os.path.join(search_dir, LEGACY_PROJECT_STRUCT_FILE)) + + if os.path.isfile(canonical): + if os.path.isfile(legacy): + logger.info( + "Both %s and %s exist; using %s. Rename or remove %s to avoid ambiguity.", + CANONICAL_PROJECT_STRUCT_FILE, + LEGACY_PROJECT_STRUCT_FILE, + CANONICAL_PROJECT_STRUCT_FILE, + LEGACY_PROJECT_STRUCT_FILE, + ) + chosen = canonical + elif os.path.isfile(legacy): + logger.info( + "Using legacy %s; prefer renaming it to %s.", + LEGACY_PROJECT_STRUCT_FILE, + CANONICAL_PROJECT_STRUCT_FILE, + ) + chosen = legacy + else: + chosen = canonical + + if file_scheme: + return f"file://{chosen}" + if explicit_path and os.path.dirname(explicit_path) in ("", "."): + # Keep a bare filename when the caller passed one (argparse default). + return os.path.basename(chosen) + if explicit_path is None and search_dir in ("", "."): + return os.path.basename(chosen) + return chosen diff --git a/tests/test_init_command.py b/tests/test_init_command.py index e7a88ad..e40ce5c 100644 --- a/tests/test_init_command.py +++ b/tests/test_init_command.py @@ -1,8 +1,7 @@ import argparse -import os from unittest.mock import patch -from structkit.commands.init import InitCommand, BASIC_STRUCTKIT_YAML +from structkit.commands.init import InitCommand def test_init_creates_struct_yaml(tmp_path): @@ -12,10 +11,10 @@ def test_init_creates_struct_yaml(tmp_path): target_dir = tmp_path / "proj" args = parser.parse_args([str(target_dir)]) - with patch('builtins.print') as mock_print: + with patch('builtins.print'): cmd.execute(args) - struct_file = target_dir / '.struct.yaml' + struct_file = target_dir / '.structkit.yaml' assert struct_file.exists() content = struct_file.read_text() @@ -34,7 +33,7 @@ def test_init_skips_if_exists(tmp_path): target_dir = tmp_path / "proj" target_dir.mkdir(parents=True) - existing = target_dir / '.struct.yaml' + existing = target_dir / '.structkit.yaml' existing.write_text('files: []\n') args = parser.parse_args([str(target_dir)]) @@ -45,4 +44,4 @@ def test_init_skips_if_exists(tmp_path): assert existing.read_text() == 'files: []\n' # Should print a message about skipping printed = "\n".join(c.args[0] for c in mock_print.call_args_list) - assert '.struct.yaml already exists' in printed + assert '.structkit.yaml already exists' in printed diff --git a/tests/test_project_file.py b/tests/test_project_file.py new file mode 100644 index 0000000..55928e7 --- /dev/null +++ b/tests/test_project_file.py @@ -0,0 +1,166 @@ +import argparse + +from structkit.commands.generate import GenerateCommand +from structkit.commands.init import InitCommand +from structkit.project_file import ( + CANONICAL_PROJECT_STRUCT_FILE, + LEGACY_PROJECT_STRUCT_FILE, + find_existing_project_struct_file, + is_default_project_struct_path, + resolve_project_struct_file, + strip_file_scheme, +) + + +def test_strip_file_scheme(): + assert strip_file_scheme("file://.structkit.yaml") == (".structkit.yaml", True) + assert strip_file_scheme(".struct.yaml") == (".struct.yaml", False) + + +def test_is_default_project_struct_path(): + assert is_default_project_struct_path(None) is True + assert is_default_project_struct_path("") is True + assert is_default_project_struct_path(".structkit.yaml") is True + assert is_default_project_struct_path(".struct.yaml") is True + assert is_default_project_struct_path("file://.struct.yaml") is True + assert is_default_project_struct_path("examples/app/.structkit.yaml") is True + assert is_default_project_struct_path("project/python") is False + assert is_default_project_struct_path("my-config.yaml") is False + + +def test_find_existing_prefers_canonical(tmp_path): + (tmp_path / CANONICAL_PROJECT_STRUCT_FILE).write_text("files: []\n") + (tmp_path / LEGACY_PROJECT_STRUCT_FILE).write_text("files: []\n") + + found = find_existing_project_struct_file(str(tmp_path)) + + assert found.endswith(CANONICAL_PROJECT_STRUCT_FILE) + + +def test_find_existing_falls_back_to_legacy(tmp_path): + (tmp_path / LEGACY_PROJECT_STRUCT_FILE).write_text("files: []\n") + + found = find_existing_project_struct_file(str(tmp_path)) + + assert found.endswith(LEGACY_PROJECT_STRUCT_FILE) + + +def test_find_existing_returns_none_when_missing(tmp_path): + assert find_existing_project_struct_file(str(tmp_path)) is None + + +def test_resolve_leaves_named_structures_unchanged(): + assert resolve_project_struct_file("project/python") == "project/python" + assert resolve_project_struct_file("file://custom.yaml") == "file://custom.yaml" + + +def test_resolve_prefers_canonical_when_both_exist(tmp_path): + (tmp_path / CANONICAL_PROJECT_STRUCT_FILE).write_text("canonical: true\n") + (tmp_path / LEGACY_PROJECT_STRUCT_FILE).write_text("legacy: true\n") + + resolved = resolve_project_struct_file(directory=str(tmp_path)) + + assert resolved.endswith(CANONICAL_PROJECT_STRUCT_FILE) + + +def test_resolve_falls_back_to_legacy_default_name(tmp_path): + (tmp_path / LEGACY_PROJECT_STRUCT_FILE).write_text("legacy: true\n") + + resolved = resolve_project_struct_file( + explicit_path=CANONICAL_PROJECT_STRUCT_FILE, + directory=str(tmp_path), + ) + + assert resolved.endswith(LEGACY_PROJECT_STRUCT_FILE) + + +def test_resolve_keeps_explicit_legacy_file_when_it_exists(tmp_path): + (tmp_path / CANONICAL_PROJECT_STRUCT_FILE).write_text("canonical: true\n") + (tmp_path / LEGACY_PROJECT_STRUCT_FILE).write_text("legacy: true\n") + explicit = str(tmp_path / LEGACY_PROJECT_STRUCT_FILE) + + resolved = resolve_project_struct_file(explicit_path=explicit, directory=str(tmp_path)) + + assert resolved == explicit + + +def test_resolve_rewrites_missing_legacy_path_to_canonical(tmp_path): + (tmp_path / CANONICAL_PROJECT_STRUCT_FILE).write_text("canonical: true\n") + explicit = str(tmp_path / LEGACY_PROJECT_STRUCT_FILE) + + resolved = resolve_project_struct_file(explicit_path=explicit, directory=str(tmp_path)) + + assert resolved.endswith(CANONICAL_PROJECT_STRUCT_FILE) + + +def test_resolve_preserves_file_scheme_on_fallback(tmp_path): + (tmp_path / LEGACY_PROJECT_STRUCT_FILE).write_text("legacy: true\n") + explicit = f"file://{tmp_path / CANONICAL_PROJECT_STRUCT_FILE}" + + resolved = resolve_project_struct_file(explicit_path=explicit, directory=str(tmp_path)) + + assert resolved.startswith("file://") + assert resolved.endswith(LEGACY_PROJECT_STRUCT_FILE) + + +def test_resolve_returns_canonical_when_neither_exists(tmp_path): + resolved = resolve_project_struct_file(directory=str(tmp_path)) + assert resolved.endswith(CANONICAL_PROJECT_STRUCT_FILE) + + +def test_generate_uses_legacy_struct_yaml_when_canonical_missing(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / LEGACY_PROJECT_STRUCT_FILE).write_text( + "files:\n - hello.txt:\n content: from-legacy\n" + ) + out_dir = tmp_path / "out" + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + args = parser.parse_args(["--non-interactive", CANONICAL_PROJECT_STRUCT_FILE, str(out_dir)]) + + command.execute(args) + + assert (out_dir / "hello.txt").read_text().strip() == "from-legacy" + + +def test_generate_prefers_structkit_yaml_when_both_exist(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / LEGACY_PROJECT_STRUCT_FILE).write_text( + "files:\n - hello.txt:\n content: from-legacy\n" + ) + (tmp_path / CANONICAL_PROJECT_STRUCT_FILE).write_text( + "files:\n - hello.txt:\n content: from-canonical\n" + ) + out_dir = tmp_path / "out" + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + args = parser.parse_args(["--non-interactive", CANONICAL_PROJECT_STRUCT_FILE, str(out_dir)]) + + command.execute(args) + + assert (out_dir / "hello.txt").read_text().strip() == "from-canonical" + + +def test_init_skips_legacy_struct_yaml(tmp_path): + parser = argparse.ArgumentParser() + cmd = InitCommand(parser) + target_dir = tmp_path / "proj" + target_dir.mkdir() + existing = target_dir / LEGACY_PROJECT_STRUCT_FILE + existing.write_text("files: []\n") + + args = parser.parse_args([str(target_dir)]) + printed = [] + + def capture(message): + printed.append(message) + + from unittest.mock import patch + with patch("builtins.print", side_effect=capture): + cmd.execute(args) + + assert existing.read_text() == "files: []\n" + assert not (target_dir / CANONICAL_PROJECT_STRUCT_FILE).exists() + output = "\n".join(printed) + assert LEGACY_PROJECT_STRUCT_FILE in output + assert CANONICAL_PROJECT_STRUCT_FILE in output diff --git a/tests/test_sources_command.py b/tests/test_sources_command.py index 7e6688e..9f0b7dd 100644 --- a/tests/test_sources_command.py +++ b/tests/test_sources_command.py @@ -208,7 +208,7 @@ def test_file_local_sources_resolve_nested_structs(monkeypatch, tmp_path): ) (local_source / "child.yaml").write_text("files:\n - child.txt:\n content: local\n") - root = tmp_path / ".struct.yaml" + root = tmp_path / ".structkit.yaml" root.write_text( "sources:\n" f" platform:\n" @@ -239,7 +239,7 @@ def test_nested_source_redefinition_is_rejected(tmp_path): "files: []\n" ) - root = tmp_path / ".struct.yaml" + root = tmp_path / ".structkit.yaml" root.write_text( "sources:\n" f" platform:\n"