Wiswa

Declarative Project Generation

The Problem

  • Starting a new project means recreating the same boilerplate every time

  • CI/CD workflows, linter configs, packaging, documentation scaffolding…​

  • Keeping all of this in sync across projects is even harder

  • Manual updates are tedious, error-prone, and easy to forget

Existing Solutions

Cookiecutter

  • Popular project templating tool

  • One-shot generation — no mechanism for pulling template updates

  • Once generated, the project drifts from the template permanently

  • Mostly used for Python projects

Cruft

  • Built on top of Cookiecutter to add updates via patching

  • Patch-based updates lead to merge conflicts with local changes

  • Sparse PyPI releases — it is common for two years or more to pass between published versions; that cadence is too slow if you want generator tooling to track a fast-moving packaging and CI ecosystem

  • Still inherits Cookiecutter’s Python-centric limitations

Common Weaknesses

  • Templates are static — no programmable logic

  • No automatic dependency version resolution

  • No integration with GitHub/GitLab APIs

  • Limited to a single language ecosystem

Introducing Wiswa

A declarative, regeneration-based project generator

  • Multi-language: Python, TypeScript, C, C++, Lua, Xcode, generic layouts, and more

  • Programmable configuration with Jsonnet

  • Automatic latest version resolution from PyPI, npm, GitHub

  • Deep GitHub and GitLab integration

How It Works: Configuration

Create a .wiswa.jsonnet in your project with only the differences from defaults:

{
  project_name: 'my-app',
  project_type: 'python',
  want_docs: true,
  // ...only what you want to change
}

How It Works: Jsonnet Basics

Jsonnet is a superset of JSON with programmable features:

Local variables Conditionals
local is_lib = self.project_type
               == 'python';
want_docs: if is_lib then true
           else false,

Deep merge with +:

Hidden fields with ::

pyproject+: {
  tool+: {
    ruff+: {
      'line-length': 120,
    },
  },
},
package_json+: {
  // :: makes it hidden in output
  license:: null,
}

Imports and library functions

 

local utils =
  import 'utils.libsonnet';

python_deps+: {
  main+: {
    click: utils
      .latestPypiPackageVersionCaret(
        'click'),
  },
},

How It Works: Merge Defaults

Merge with 400+ built-in defaults from defaults.libsonnet (and your own [1]).

defaults.libsonnet + user defaults.jsonnet + .wiswa.jsonnet

1. Controlled by uses_user_defaults in .wiswa.jsonnet — when true, merges shared personal defaults.jsonnet from the user config directory.

How It Works: Jsonnet Evaluation

Evaluate the merged configuration with native functions — version resolution, date helpers, URI generation, and more.

How It Works: File Generation

Settings map directly to output files. For example, package.json:

.wiswa.jsonnet package.json (generated)
{
  package_json+: {
    description: 'My app',
    prettier+: {
      printWidth: 120,
    },
    scripts+: {
      deploy: 'rsync -a dist/ srv:/',
    },
  },
}
{
  "description": "My app",
  "devDependencies": {
    "prettier": "^3.5.3",
    ...
  },
  "prettier": {
    "printWidth": 120,
    "singleQuote": true,
    ...
  },
  "scripts": {
    "deploy": "rsync -a dist/ srv:/",
    "format": "prettier -w . && ...",
    ...
  }
}

How It Works: Post-Processing

Lock dependencies, then run the generated project’s format and QA scripts (for example yarn format and yarn qa).

How It Works: Repository Setup

Configure your GitHub or GitLab repository via API — merge settings, branch protection, security scanning, and more.

Configuration: .wiswa.jsonnet

Only specify what differs from defaults:

local utils = import 'utils.libsonnet';
{
  project_name: 'my-app',
  project_type: 'python',
  github_username: 'myuser',
  package_manager: 'uv',

  want_docs: true,
  want_tests: true,
  want_codeql: true,
  pyproject+: {
    tool+: {
      ruff+: {
        'line-length': 100,
      },
    },
  },

  python_deps+: {
    main+: {
      click: utils
        .latestPypiPackageVersionCaret(
          'click'),
    },
  },
}

The utils.libsonnet Library

Import it in any .wiswa.jsonnet to access built-in helper functions:

local utils = import 'utils.libsonnet';
  • utils.year(), utils.isodate() — current year and ISO date

  • utils.manifestYaml(obj), utils.manifestToml(obj) — serialisation helpers

  • utils.gitHubRepositoryUri(user, project) — generate common URIs

  • utils.pyprojectAuthors(authors) — transform author metadata

  • utils.pyprojectClassifiers(settings) — generate sorted PyPI classifiers

  • utils.formatPep508Deps(name, val) — convert dependency specs to PEP 508

  • utils.poetryVerToPep508(ver) — translate ^/~ version syntax

Smart Version Resolution

The utils library also provides functions that fetch the latest versions at generation time:

local utils = import 'utils.libsonnet';

// PyPI (caret, tilde, >= prefixed, or bare)
utils.latestPypiPackageVersionCaret('click')   // "^8.1.8"
utils.latestPypiPackageVersionTilde('django')  // "~5.2.1"
utils.latestPypiPackageVersionGe('requests')   // ">=2.32.3"

// npm
utils.latestNpmPackageVersion('typescript')    // "5.7.3"
utils.latestNpmPackageVersionCaret('eslint')   // "^9.20.0"

// GitHub releases and tags
utils.githubLatestReleaseTag('owner', 'repo')  // "v2.4.1"
utils.githubLatestActionTag('actions', 'checkout') // "v4"
utils.githubLatestTag('owner', 'repo')         // "v3.1.0"

// Yarn
utils.latestYarnVersion()                      // "4.6.0"

The Update Workflow

  1. Make changes to .wiswa.jsonnet (or re-run to pick up new defaults)

  2. Run wiswa to regenerate all managed files (set uses_user_defaults: true to merge user-level defaults.jsonnet)

  3. Post-processing locks dependencies and runs yarn format, yarn qa, and related steps

  4. Review the git diff

  5. Commit

Key insight: regeneration, not patching.

No merge conflicts. Managed files are always overwritten from templates and settings. Post-processing still runs yarn format, yarn qa, and similar; anything those scripts run with auto-fix will change sources when fixes apply.

Bidirectional Sync

What if you change a managed file directly?

  • Example: uv add requests modifies pyproject.toml

  • The wiswa-sync agent reflects the change back into .wiswa.jsonnet

  • Next regeneration preserves your addition

  • Configuration and files stay in sync in both directions

What Gets Generated?

Category Examples

Build & packaging

pyproject.toml, package.json, CMakeLists.txt, vcpkg.json

CI/CD

GitHub Actions workflows, GitLab CI, Dependabot config

Code quality

Ruff, ESLint, clang-format, pre-commit hooks, mypy

Documentation

Sphinx + ReadTheDocs, GitHub Pages, CONTRIBUTING, SECURITY

IDE

VS Code settings, launch configs, recommended extensions

AI tooling

AGENTS.md, CLAUDE.md, .claude/ agents, rules, skills, local settings (optional)

Distribution

AppImage, PyInstaller, Snap, Flatpak, winget

Badges

README.md; Sphinx docs/badges.rst (Python projects only, when docs are enabled)

Multi-Language Support

Language Highlights

Python

pyproject.toml, uv/Poetry, Ruff, mypy, pytest, Sphinx, PyPI publishing

TypeScript

package.json, Jest, ESLint, Prettier, npm publishing

C / C++

CMake, CMakePresets, vcpkg, clang-format

Lua

luacheck, Busted tests, LuaRocks publishing

Xcode (project_type: 'xcode')

clang-format, Xcode-oriented defaults

Generic

package.json, Prettier, basic CI

GitHub / GitLab Integration

Wiswa configures your repository via API:

  • Merge settings (squash, rebase, delete branch on merge)

  • Branch protection and commit signature requirements

  • Secret scanning and push protection

  • Pages deployment (GitHub Pages / GitLab Pages)

  • Dependabot security updates (GitHub only)

  • CodeQL security analysis (GitHub only)

The MCP Server

Wiswa exposes its configuration system as an MCP server for AI integration.

claude mcp add wiswa-mcp -- wiswa-mcp

MCP Server: Tools

Four tools available:

  • get_defaults(key_path) — retrieve resolved default settings

  • lookup_setting(key_path) — get a value with its Jsonnet override snippet

  • list_settings(key_path, depth) — browse the settings hierarchy

  • search_settings(query) — full-text search across all settings

MCP Server: Example

> lookup_setting("pyproject.tool.ruff.line-length")

{
  "key_path": "pyproject.tool.ruff.line-length",
  "default_value": 100,
  "type": "number",
  "override_snippet": "pyproject+: { tool+: { ruff+: { 'line-length': <YOUR_VALUE> } } }",
  "notes": []
}

AI assistants can explore and modify Wiswa configuration programmatically.

Customisability

  • Jsonnet merge operators (+:) for deep, surgical overrides

  • User-level defaults (~/.config/wiswa/defaults.jsonnet) shared across all projects

  • Feature flags (want_docs, want_tests, want_main, want_codeql, …​)

  • -J flag to add directories to the Jsonnet search path for custom libraries

Architecture Overview

wiswa/
  main.py              -- async orchestrator
  mcp.py               -- FastMCP server
  session.py           -- filesystem-cached niquests session
  utils/
    jsonnet.py         -- Jsonnet eval + native callbacks
    versions.py        -- package version fetching
    postprocess.py     -- formatting, locking, cleanup

wiswa-jsonnet/
  defaults.libsonnet  -- base defaults (400+ fields)
  project.jsonnet      -- generates all config files
  utils.libsonnet     -- helper functions

Summary

  • Declarative: one .wiswa.jsonnet file describes your entire project setup

  • Regeneration-based: no merge conflicts, always up to date

  • Multi-language: Python, TypeScript, C/C++, Lua, Xcode, generic

  • Smart: automatic version resolution, GitHub/GitLab API integration

  • Extensible: Jsonnet configuration, user defaults, MCP server

  • AI-native: MCP server, Claude Code agents, Copilot/Cursor instructions