Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0b97ca4
resolve EVO-005
amol- May 14, 2026
f2b5ff0
resolve EVO-007
amol- May 14, 2026
4d05124
resolve EVO-008
amol- May 14, 2026
6a9773c
resolve EVO-012
amol- May 14, 2026
28c7e1b
resolve EVO-009
amol- May 14, 2026
96652ec
resolve EVO-010
amol- May 14, 2026
73fcb7e
resolve EVO-011
amol- May 14, 2026
ab5e5e8
resolve EVO-013
amol- May 14, 2026
2dcbcf2
resolve EVO-014
amol- May 14, 2026
3441879
resolve EVO-015
amol- May 14, 2026
ede7b9a
resolve EVO-016
amol- May 14, 2026
c906b90
resolve EVO-017
amol- May 14, 2026
47ab057
resolve EVO-018
amol- May 14, 2026
998762a
resolve EVO-020
amol- May 15, 2026
aa6cfb6
skills for agents instead of mcp
amol- Jul 15, 2026
040ee8b
cleanup after transition to skills
amol- Jul 15, 2026
ecd43b3
consolidate tginfo
amol- Jul 15, 2026
2e467b9
cleanups
amol- Jul 16, 2026
6b44d1f
Refactor as subcommands
amol- Jul 16, 2026
f161349
fix scaffolds scan
amol- Jul 16, 2026
2edf79d
migrate to print
amol- Jul 16, 2026
502007e
lookup table
amol- Jul 16, 2026
5b9aeb5
better encapsulation
amol- Jul 16, 2026
b5d3853
Simplify tginfo by reducing unecessary defensive programming
amol- Jul 16, 2026
ba720e1
Some further cleanups
amol- Jul 16, 2026
488a7e5
replace direct access to sys.stdX with print calls
amol- Jul 16, 2026
8e764bd
Improve skills for dumber models
amol- Jul 20, 2026
f6d47b1
propagate commands failures to shell exit codes
amol- Jul 31, 2026
bc46466
improve tg-shell skill
amol- Aug 1, 2026
990229d
output to stderr
amol- Aug 1, 2026
cbcce56
rely on repr of entities
amol- Aug 2, 2026
d06bcff
reuse tg.util.callable_name
amol- Aug 2, 2026
0bccffc
note that tginfo might trigger side-effects
amol- Aug 3, 2026
c668fa9
include gitignore in quickstarts
amol- Aug 3, 2026
1921bbf
improve README and enable project commands in api quickstart
amol- Aug 3, 2026
1e993cd
Guarantee AGENTS.md in api quickstart too
amol- Aug 3, 2026
11a53f5
tweak readmes
amol- Aug 3, 2026
6861976
improve instructions on setup of devtools
amol- Aug 3, 2026
49cd20a
improve scaffolds for quickstart
amol- Aug 4, 2026
e2adb87
Improve instructions for agents
amol- Aug 6, 2026
ec3c77c
Install project-local agent skills by default
amol- Aug 6, 2026
248d39a
require pytest for testing
amol- Aug 11, 2026
afe3a49
output contract for inspect skill
amol- Aug 11, 2026
a04efa4
Improve agents support
amol- Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.mak.py
*.pyc
*.pyo
__pycache__/
*.swp
TESTENV
TGTest-*
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
recursive-include devtools/gearbox/*/template *
recursive-include devtools/gearbox/quickstart/template-api *
recursive-include devtools/gearbox/quickstart/patches *
recursive-include devtools/gearbox/tgskills/skills *

global-exclude __pycache__/*
global-exclude *.pyc
48 changes: 47 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Key Features
- **Database Migrations**: Run migration commands for SQLAlchemy and Alembic effortlessly.
- **Interactive Shell**: Launch a shell preloaded with your app's context for rapid testing.
- **Internationalization**: Extract, initialize, update, and compile translation catalogs for your application.
- **Agent Inspection**: Inspect routes, models, templates, and scaffold templates with ``tginfo``.
- **Agent Skills**: Install TurboGears-aware agent skills for AI coding assistants with ``gearbox tgskills``.
- **Gearbox Integration**: Seamlessly work with Gearbox to serve and manage your applications.

Getting Started
Expand Down Expand Up @@ -78,7 +80,50 @@ Usage Examples

::

gearbox tgshell
gearbox tgshell -c development.ini

- **Inspect a Project for Agents and Scripts:**

::

gearbox tginfo summary --project . --config development.ini
gearbox tginfo routes --project . --config development.ini --json
gearbox tginfo models --project . --config development.ini --json
gearbox tginfo templates --project . --config development.ini --json
gearbox tginfo scaffolds --project . --config development.ini --json

``tginfo`` is read-only. It can import the target application just like
``gearbox tgshell`` or ``gearbox serve``, but it does not run ``setup-app``,
migrations, database writes, or runtime requests as part of inspection.
Loading application code can still execute project-defined import/startup side
effects, like any Python module.

- **Install Agent Skills:**

::

gearbox tgskills

By default this installs agent skills into the project-local
``.agents/skills/`` directory. To opt in manually to Claude Code instead, run
``gearbox tgskills --claude``; this installs only ``.claude/skills/``. The
skills tell agents to use ``gearbox tginfo`` for inspection,
``gearbox scaffold`` for creating conventional project files, and
``gearbox tgshell`` for runtime debugging with WebTest. To remove installed
skills, delete the project-local ``.agents/skills/`` directory or, for a
Claude Code installation, the ``.claude/skills/`` directory.

- **Runtime Debugging:**

Use ``gearbox tgshell -c development.ini`` when you need the fully loaded
application context for runtime checks, including WebTest requests. Runtime
request debugging belongs in ``tgshell`` rather than ``tginfo``.

- **Safety Boundaries:**

``tginfo`` inspection tools are read-only. Do not run ``setup-app``,
migrations, or other database-mutating commands as routine inspection; use
them only when intentionally changing a development or test environment.

- **Manage Translations:**

Expand All @@ -93,6 +138,7 @@ Resources
---------
- **TurboGears Website**: `http://www.turbogears.org`
- **Documentation**: `https://turbogears.readthedocs.io`
- **Agent Skills**: Agent Skills standard at `https://agentskills.io`
- **Community & Support**: Join our `Mailing List <http://groups.google.com/group/turbogears>`_ or `Gitter Chatroom <https://gitter.im/turbogears/Lobby>`_ chatroom.

Contributing
Expand Down
46 changes: 34 additions & 12 deletions devtools/gearbox/quickstart/command.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
import os
import shutil
import sys
import uuid
import importlib.metadata
import importlib.util
Expand All @@ -9,6 +10,8 @@
from gearbox.template import GearBoxTemplate
from gearbox.command import Command

from ..tgskills import TgSkillsCommand

beginning_letter = re.compile(r"^[^a-z]*")
valid_only = re.compile(r"[^a-z0-9_]")

Expand All @@ -27,8 +30,14 @@ def pre(self, command, output_dir, vars):
if vars['migrations']:
vars['egg_plugins'].append('tg.devtools')

def post(self, template_dir, output_dir, vars):
if not vars['database']:
os.remove(
os.path.join(output_dir, vars['package'], 'model', 'model.py.template')
)


class QuickstartAPITemplate(GearBoxTemplate):
class QuickstartAPITemplate(QuickstartTemplate):

def pre(self, command, output_dir, vars):
"""Called before API template is applied."""
Expand Down Expand Up @@ -120,20 +129,22 @@ def take_action(self, opts):
except importlib.metadata.PackageNotFoundError:
pass
else:
print('The name "%s" is already in use' % opts.name)
print('The name "%s" is already in use' % opts.name,
file=sys.stderr)
return 1

try:
if importlib.util.find_spec(opts.package):
print('The package name "%s" is already in use'
% opts.package)
return
% opts.package, file=sys.stderr)
return 1
except ImportError:
pass

if os.path.exists(opts.name):
print('A directory called "%s" already exists. Exiting.'
% opts.name)
return
% opts.name, file=sys.stderr)
return 1

opts.cookiesecret = str(uuid.uuid4())
opts.passwordsalt = str(uuid.uuid4())
Expand Down Expand Up @@ -173,6 +184,8 @@ def take_action(self, opts):
package_migrations_dir = os.path.abspath('migration')
shutil.rmtree(package_migrations_dir, ignore_errors=True)

_install_project_skills()


def safe_name(name: str) -> str:
"""Convert an arbitrary string to a standard distribution name
Expand Down Expand Up @@ -262,21 +275,22 @@ def take_action(self, opts):
except importlib.metadata.PackageNotFoundError:
pass
else:
print('The name "%s" is already in use' % opts.name)
return
print('The name "%s" is already in use' % opts.name,
file=sys.stderr)
return 1

try:
if importlib.util.find_spec(opts.package):
print('The package name "%s" is already in use'
% opts.package)
return
% opts.package, file=sys.stderr)
return 1
except ImportError:
pass

if os.path.exists(opts.name):
print('A directory called "%s" already exists. Exiting.'
% opts.name)
return
% opts.name, file=sys.stderr)
return 1

opts.cookiesecret = str(uuid.uuid4())
opts.passwordsalt = str(uuid.uuid4())
Expand Down Expand Up @@ -312,3 +326,11 @@ def take_action(self, opts):
# remove existing migrations directory
package_migrations_dir = os.path.abspath('migration')
shutil.rmtree(package_migrations_dir, ignore_errors=True)

_install_project_skills()


def _install_project_skills():
command = TgSkillsCommand(None, {})
opts = command.get_parser('gearbox tgskills').parse_args([])
command.run(opts)
75 changes: 75 additions & 0 deletions devtools/gearbox/quickstart/template-api/+dot+gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# Database
*.sqlite
*.db

# Agent skills (machine-local, regenerated from the installed devtools package)
.agents/
.claude/
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
"""REST controller for {{start_braces}}target.lower(){{end_braces}}."""

from tg import expose
from tg.controllers import RestController


class {{start_braces}}target.capitalize(){{end_braces}}Controller(RestController):
"""REST endpoints for {{start_braces}}target.lower(){{end_braces}}."""

@expose('json')
def get_all(self):
"""Return the {{start_braces}}target.lower(){{end_braces}} collection."""
return dict({{start_braces}}target.lower(){{end_braces}}=[])
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""{{start_braces}}target.capitalize(){{end_braces}} model module."""

{{if sqlalchemy}}
from sqlalchemy import *
from sqlalchemy import Table, ForeignKey, Column
from sqlalchemy.types import Integer, Unicode, DateTime, LargeBinary
from sqlalchemy.orm import relationship, backref

from {{package}}.model import DeclarativeBase, metadata, DBSession


class {{start_braces}}target.capitalize(){{end_braces}}(DeclarativeBase):
__tablename__ = '{{start_braces}}target.lower(){{end_braces}}s'

uid = Column(Integer, primary_key=True)
data = Column(Unicode(255), nullable=False)

{{if auth}}
user_id = Column(Integer, ForeignKey('tg_user.user_id'), index=True)
user = relationship('User', uselist=False,
backref=backref('{{start_braces}}target.lower(){{end_braces}}s',
cascade='all, delete-orphan'))
{{endif}}

{{else}}
from ming import schema as s
from ming.odm import FieldProperty, ForeignIdProperty, RelationProperty
from ming.odm.declarative import MappedClass

from {{package}}.model import DBSession


class {{start_braces}}target.capitalize(){{end_braces}}(MappedClass):
class __mongometa__:
session = DBSession
name = '{{start_braces}}target.lower(){{end_braces}}s'
indexes = [
('_user',),
]

_id = FieldProperty(s.ObjectId)

name = FieldProperty(s.String, required=False)

{{if auth}}
_user = ForeignIdProperty('User')
user = RelationProperty('User')
{{endif}}
{{endif}}


__all__ = ['{{start_braces}}target.capitalize(){{end_braces}}']
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{start_braces}}# gearbox: output-stem=test_{target} {{end_braces}}
# -*- coding: utf-8 -*-
"""Functional tests for the {{start_braces}}target.capitalize(){{end_braces}} API controller."""

from {{package}}.tests import TestController, setup_database


class Test{{start_braces}}target.capitalize(){{end_braces}}Controller(TestController):
"""Tests for the mounted {{start_braces}}target.lower(){{end_braces}} API controller."""

headers = {'Accept': 'application/json'}

def setup_method(self, method):
super().setup_method(method)
setup_database()

def test_get_all(self):
response = self.app.get('/api/{{start_braces}}target.lower(){{end_braces}}', headers=self.headers)

assert response.content_type == 'application/json'
assert '{{start_braces}}target.lower(){{end_braces}}' in response.json
Loading
Loading