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"