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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions skills/catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"name": "stac-quickstart",
"description": "Initialize and validate a Stac-enabled Flutter project and run first deploy.",
"path": "skills/stac-quickstart",
"default_prompt": "Use $stac-quickstart to initialize my Flutter app with Stac and deploy a first screen."
},
{
"name": "stac-screen-builder",
"description": "Build and scaffold Stac DSL screens and themes from product requirements.",
"path": "skills/stac-screen-builder",
"default_prompt": "Use $stac-screen-builder to turn product requirements into Stac DSL screens and themes."
},
{
"name": "stac-custom-extensions",
"description": "Scaffold custom Stac widget/action models and parser integrations.",
"path": "skills/stac-custom-extensions",
"default_prompt": "Use $stac-custom-extensions to scaffold a custom widget or action parser for my Stac app."
},
{
"name": "stac-troubleshooter",
"description": "Diagnose Stac build, deploy, rendering, caching, and navigation issues.",
"path": "skills/stac-troubleshooter",
"default_prompt": "Use $stac-troubleshooter to diagnose why my Stac screen is not building, deploying, or rendering."
}
]
51 changes: 51 additions & 0 deletions skills/stac-custom-extensions/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
name: stac-custom-extensions
description: Scaffold and integrate custom Stac widgets and actions with parsers and registration checks. Use when users ask to build new StacParser or StacActionParser implementations, generate custom model classes, or verify parser registration inside Stac.initialize.
---

# Stac Custom Extensions

## Overview

Use this skill to add custom widgets/actions that serialize cleanly and register correctly in a Stac app.

## Workflow

1. Choose extension type: widget or action.
2. Scaffold model and parser files with scripts.
3. Add generated files to app codebase and run codegen.
4. Verify parser registration in `main.dart`.
5. Validate runtime wiring with a minimal usage example.

## Required Inputs

- PascalCase extension name.
- Runtime type id (`type` or `actionType`).
- Output directory for generated files.
- Path to `main.dart` for registration check.

## Output Contract

- Produce model + parser pair with consistent type ids.
- Include registration snippet for `Stac.initialize`.
- Include `build_runner` command when json serialization is used.

## References

- Read `references/custom-widget-checklist.md` for widget model/parser flow.
- Read `references/custom-action-checklist.md` for action model/parser flow.
- Read `references/parser-registration.md` for initialization wiring.
- Read `references/converters-guide.md` for converter usage patterns.

## Scripts

- `scripts/scaffold_custom_widget.py --name <Name> --type <widgetType> --out-dir <path>`
- `scripts/scaffold_custom_action.py --name <Name> --action-type <actionType> --out-dir <path>`
- `scripts/check_parser_registration.py --main-dart <path> --parser-class <ClassName>`

## Templates

- `assets/templates/custom_widget.dart.tmpl`
- `assets/templates/custom_widget_parser.dart.tmpl`
- `assets/templates/custom_action.dart.tmpl`
- `assets/templates/custom_action_parser.dart.tmpl`
5 changes: 5 additions & 0 deletions skills/stac-custom-extensions/agents/interface.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface:
display_name: "Stac Custom Extensions"
short_description: "Build custom Stac widgets/actions with parsers"
brand_color: "#15803D"
default_prompt: "Use $stac-custom-extensions to scaffold a custom widget or action parser for my Stac app."
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stac_core/stac_core.dart';

part '__FILE_BASENAME__.g.dart';

@JsonSerializable()
class __CLASS_NAME__ extends StacAction {
const __CLASS_NAME__({
this.message,
});

final String? message;

@override
String get actionType => '__ACTION_TYPE__';

factory __CLASS_NAME__.fromJson(Map<String, dynamic> json) =>
_$__CLASS_NAME__FromJson(json);

@override
Map<String, dynamic> toJson() => _$__CLASS_NAME__ToJson(this);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:stac_framework/stac_framework.dart';
import '__FILE_BASENAME__.dart';

class __PARSER_CLASS__ implements StacActionParser<__CLASS_NAME__> {
const __PARSER_CLASS__();

@override
String get actionType => '__ACTION_TYPE__';

@override
__CLASS_NAME__ getModel(Map<String, dynamic> json) =>
__CLASS_NAME__.fromJson(json);

@override
FutureOr<dynamic> onCall(BuildContext context, __CLASS_NAME__ model) {
debugPrint(model.message ?? '__CLASS_NAME__ called');
// TODO: Implement action logic
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stac_core/stac_core.dart';

part '__FILE_BASENAME__.g.dart';

@JsonSerializable()
class __CLASS_NAME__ extends StacWidget {
const __CLASS_NAME__({
this.label,
});

final String? label;

@override
String get type => '__WIDGET_TYPE__';

factory __CLASS_NAME__.fromJson(Map<String, dynamic> json) =>
_$__CLASS_NAME__FromJson(json);

@override
Map<String, dynamic> toJson() => _$__CLASS_NAME__ToJson(this);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:stac_framework/stac_framework.dart';
import '__FILE_BASENAME__.dart';

class __PARSER_CLASS__ extends StacParser<__CLASS_NAME__> {
const __PARSER_CLASS__();

@override
String get type => '__WIDGET_TYPE__';

@override
__CLASS_NAME__ getModel(Map<String, dynamic> json) =>
__CLASS_NAME__.fromJson(json);

@override
Widget parse(BuildContext context, __CLASS_NAME__ model) {
return Text(model.label ?? '__CLASS_NAME__');
}
}
13 changes: 13 additions & 0 deletions skills/stac-custom-extensions/references/converters-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Converters Guide

## DoubleConverter

Use when JSON may send integer values for `double` fields.

## StacWidgetConverter

Use for child widget fields in custom widget models.

## General Rule

Use converters only when field serialization would otherwise fail or lose fidelity.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Custom Action Checklist

1. Define action model extending `StacAction`.
2. Set unique `actionType` string.
3. Add `@JsonSerializable()` and `part` file.
4. Implement `fromJson` and `toJson`.
5. Create parser implementing `StacActionParser<Model>`.
6. Register parser in `Stac.initialize(actionParsers: [...])`.
7. Trigger action from widget callback (`onPressed`, `onTap`, etc.).
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Custom Widget Checklist

1. Define widget model extending `StacWidget`.
2. Set unique `type` string.
3. Add `@JsonSerializable()` and `part` file.
4. Implement `fromJson` and `toJson`.
5. Create parser extending `StacParser<Model>`.
6. Register parser in `Stac.initialize(parsers: [...])`.
7. Run:

```bash
dart run build_runner build --delete-conflicting-outputs
```
27 changes: 27 additions & 0 deletions skills/stac-custom-extensions/references/parser-registration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Parser Registration

## Widget Parser

```dart
await Stac.initialize(
options: defaultStacOptions,
parsers: const [
MyWidgetParser(),
],
);
```

## Action Parser

```dart
await Stac.initialize(
options: defaultStacOptions,
actionParsers: const [
MyActionParser(),
],
);
```

## Validation Rule

- Parser class name should appear in the same file that calls `Stac.initialize`.
54 changes: 54 additions & 0 deletions skills/stac-custom-extensions/scripts/check_parser_registration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Check that a parser class appears in the same file as Stac.initialize."""

from __future__ import annotations

import argparse
from pathlib import Path
import sys


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Validate parser registration in a main.dart file."
)
parser.add_argument("--main-dart", required=True, help="Path to main.dart")
parser.add_argument(
"--parser-class", required=True, help="Parser class to locate in file"
)
return parser.parse_args()


def main() -> int:
args = parse_args()
main_dart = Path(args.main_dart).expanduser().resolve()

if not main_dart.exists():
print(f"[FAIL] File not found: {main_dart}")
return 1

content = main_dart.read_text(encoding="utf-8", errors="ignore")
init_index = content.find("Stac.initialize(")
parser_index = content.find(args.parser_class)

if init_index < 0:
print("[FAIL] Stac.initialize(...) call not found")
return 1

if parser_index < 0:
print(f"[FAIL] Parser class not found: {args.parser_class}")
return 1

if parser_index < init_index:
print(
"[WARN] Parser class appears before initialize call; verify registration location manually"
)
else:
print(f"[OK] Found parser class near/after initialize call: {args.parser_class}")

print("Registration check completed.")
return 0


if __name__ == "__main__":
sys.exit(main())
96 changes: 96 additions & 0 deletions skills/stac-custom-extensions/scripts/scaffold_custom_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Scaffold custom action model and parser files from templates."""

from __future__ import annotations

import argparse
from pathlib import Path
import re
import sys


def to_snake_case(name: str) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Scaffold custom Stac action files.")
parser.add_argument("--name", required=True, help="PascalCase class name.")
parser.add_argument(
"--action-type", required=True, help="Stac action type identifier."
)
parser.add_argument("--out-dir", required=True, help="Output directory.")
parser.add_argument("--force", action="store_true", help="Overwrite existing files.")
return parser.parse_args()


def render(template: str, mapping: dict[str, str]) -> str:
content = template
for key, value in mapping.items():
content = content.replace(key, value)
return content


def write_file(path: Path, content: str, force: bool) -> None:
if path.exists() and not force:
raise FileExistsError(f"File already exists: {path}")
path.write_text(content, encoding="utf-8")


def main() -> int:
args = parse_args()

if not re.fullmatch(r"[A-Z][A-Za-z0-9]*", args.name):
print("[FAIL] --name must be PascalCase.")
return 1
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", args.action_type):
print("[FAIL] --action-type must start with a letter and use alphanumeric/_ only.")
return 1

skill_root = Path(__file__).resolve().parents[1]
model_template = (skill_root / "assets/templates/custom_action.dart.tmpl").read_text(
encoding="utf-8"
)
parser_template = (
skill_root / "assets/templates/custom_action_parser.dart.tmpl"
).read_text(encoding="utf-8")

file_basename = f"{to_snake_case(args.name)}_action"
parser_class = f"Stac{args.name}ActionParser"
mapping = {
"__CLASS_NAME__": args.name,
"__ACTION_TYPE__": args.action_type,
"__FILE_BASENAME__": file_basename,
"__PARSER_CLASS__": parser_class,
}

out_dir = Path(args.out_dir).expanduser().resolve()
out_dir.mkdir(parents=True, exist_ok=True)

model_file = out_dir / f"{file_basename}.dart"
parser_file = out_dir / f"{file_basename}_parser.dart"

# Pre-check both files before writing to avoid partial creation
if not args.force:
if model_file.exists():
print(f"[FAIL] File already exists: {model_file}")
return 1
if parser_file.exists():
print(f"[FAIL] File already exists: {parser_file}")
return 1

try:
write_file(model_file, render(model_template, mapping), args.force)
write_file(parser_file, render(parser_template, mapping), args.force)
except FileExistsError as exc:
# This should not happen due to pre-check, but handle it anyway
print(f"[FAIL] {exc}")
return 1

print(f"[OK] Created {model_file}")
print(f"[OK] Created {parser_file}")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading