name: new-app
description: This skill should be used when the user wants to scaffold a new Flux-managed app in this home-ops repo — phrases like "create a new app", "scaffold a new app", "add a new app to the cluster", or when the user invokes /new-app. The skill copies parameterized templates from .claude/skills/new-app/templates/ into apps/<name>/, asks the user about Postgres, Dragonfly, ingress exposure, and Anubis, and registers the new app in apps/kustomization.yaml.
new-app
Scaffold a new Flux-managed app under apps/<name>/ by copying the templates in .claude/skills/new-app/templates/. The templates were derived from apps/ideon and contain all the # TODO: markers the user must address before committing.
Inputs
The skill needs a single positional argument: the app name. The user may supply it inline (/new-app foo) or you must ask for it.
Validate the name:
- lowercase letters, digits, and hyphens only (
^[a-z][a-z0-9-]*$) - not empty
apps/<name>/must not already exist (check withtest -e)
If invalid or already taken, stop and report.
Derive two substitution values:
<app>= the lowercase name (e.g.simple-todo)<APP_UPPER>= uppercase, with-replaced by_(e.g.SIMPLE_TODO)
Interactive questions
Use the AskUserQuestion tool to ask all of the following in one call (a single tool invocation with multiple questions in the questions array — do not ask them one at a time).
- Postgres — "Does this app need a Postgres database (the default cluster at
postgres-rw.default.svc.cluster.local)?" — yes / no - Other Bitwarden secret — "Does this app need a non-Postgres secret pulled from Bitwarden Secrets Manager (e.g. API keys, OAuth client secret)?" — yes / no
- Dragonfly — "Does this app need a Dragonfly (Redis-compatible) cache via the shared component?" — yes / no
- Persistent volume — "Does this app need a PersistentVolumeClaim for on-disk state?" — yes / no
- Exposure — "How is this app exposed?" —
internal(envoy-internal only) /external-plain(envoy-external, no Anubis) /external-anubis(envoy-external behind the Anubis challenge)
If the user answered yes to "Other Bitwarden secret", ask a follow-up (separate AskUserQuestion call) for the Bitwarden Secrets Manager key name (defaults to <app> if they don't specify).
If the user answered external-anubis, ask a follow-up for the internal service port that Anubis should target. The default is the app's service.main.ports.http.port — usually 3000. The resulting ANUBIS_TARGET will be http://<app>:<port>.
File scaffolding
Read every file in .claude/skills/new-app/templates/ and write transformed copies under apps/<app>/ at the same relative path. For each file:
- Substitute
__APP__→<app>(literal text replace, every occurrence). - Substitute
__APP_UPPER__→<APP_UPPER>. - Apply the option-specific transforms below by replacing the placeholder lines (the lines beginning with
# __FOO_BLOCK__or the bare placeholders__ROUTE_DOMAIN__/__ROUTE_PARENT__/__BITWARDEN_KEY__).
Important: when a placeholder block is removed, also delete the preceding blank line if it produces double blank lines (keep the file tidy).
Per-file transforms
ks.yaml
# __COMPONENTS_BLOCK__— replace withcomponents:+ indented entries based on options. Order: dragonfly first, then anubis. If neither dragonfly nor anubis is enabled, delete this line entirely (nocomponents:field).- Dragonfly entry:
- ../../../components/dragonfly - Anubis entry:
- ../../../components/anubis
- Dragonfly entry:
# __SUBSTITUTE_EXTRAS__— replace with anubis-related substitutions if anubis is enabled, otherwise delete the line.- Anubis substitutions (indented under
substitute:, same indent asAPP:):APP_NAMESPACE: *namespace ANUBIS_TARGET: http://<app>:<port>
- Anubis substitutions (indented under
app/kustomization.yaml
# __RESOURCES_EXTRAS__— replace with the additional resource entries that apply, otherwise delete the line:- externalsecret.yamlif Postgres OR other-Bitwarden-secret is enabled.- pvc.yamlif persistent volume is enabled.
Note: there is a single
externalsecret.yamlfile regardless of whether Postgres is enabled — it holds bothExternalSecrets when Postgres is on (see below).
app/helmrelease.yaml
# __INIT_CONTAINERS_BLOCK__— if Postgres is enabled, replace with:
Otherwise delete the line.initContainers: init-db: image: repository: ghcr.io/home-operations/postgres-init tag: 18.3 pullPolicy: IfNotPresent envFrom: - secretRef: name: <app>-init-db# __DRAGONFLY_ENV_BLOCK__— if Dragonfly is enabled, replace with:
Otherwise delete the line.REDIS_HOST: <app>-dragonfly REDIS_PORT: 6379# __ENVFROM_BLOCK__— if the app has any ExternalSecret (Postgres OR other-Bitwarden), replace with:
Otherwise delete the line.envFrom: - secretRef: name: *app__ROUTE_DOMAIN__—SECRET_INTERNAL_DOMAINfor internal exposure,SECRET_EXTERNAL_DOMAINfor external exposure (with or without Anubis). Wrap as${SECRET_INTERNAL_DOMAIN}/${SECRET_EXTERNAL_DOMAIN}— the final line should readhostnames: [ "${APP}.${SECRET_..._DOMAIN}" ].__ROUTE_PARENT__—envoy-internalfor internal,envoy-externalfor external (with or without Anubis).# __ROUTE_RULES_BLOCK__— if Anubis is enabled, replace with:
Otherwise delete the line.rules: - backendRefs: - name: <app>-anubis port: 8080# __PERSISTENCE_BLOCK__— if persistent volume is enabled, replace with:
Otherwise delete the line.persistence: data: enabled: true existingClaim: *app advancedMounts: main: main: # TODO: update mount path to match the app's data directory - path: /app/storage tmpfs: type: emptyDir sizeLimit: 100Gi globalMounts: - path: /cache subPath: cache - path: /tmp subPath: tmp - path: /run subPath: run
app/externalsecret.yaml
Write this file only if Postgres is enabled OR other-Bitwarden-secret is enabled. There are three sub-cases:
- Other-Bitwarden only (no Postgres): copy the template as-is, substitute
__APP__/__APP_UPPER__, and replace__BITWARDEN_KEY__with the user-supplied key (default<app>). - Postgres only (no other Bitwarden secret): write the postgres template (
templates/app/externalsecret-postgres.yaml) under the nameapp/externalsecret.yaml. Substitute__APP__/__APP_UPPER__. Remove the__BITWARDEN_KEY__placeholder line — postgres template doesn't use it. - Both: write both
ExternalSecrets concatenated (with---separator) into a singleapp/externalsecret.yaml. First doc = the generic template; second doc = the postgres template. Substitute placeholders in each.
In all cases, the file is named app/externalsecret.yaml. The template named externalsecret-postgres.yaml is just a source — the output goes into externalsecret.yaml.
If neither Postgres nor other-Bitwarden secret is enabled, do not create externalsecret.yaml.
app/pvc.yaml
Write only if persistent volume is enabled. Substitute __APP__ (the PVC's metadata.name uses ${APP} via postBuild so no further changes needed).
netpols.yaml
# __INGRESS_PROMETHEUS_BLOCK__— drop the line by default. If the app exposes metrics scraped by Prometheus (the user did not get asked this — leave a# TODO:comment in its place noting they may need to add it). Specifically, replace the line with:# TODO: if Prometheus needs to scrape this namespace, uncomment: # - fromEndpoints: # - matchLabels: # app.kubernetes.io/name: prometheus # "k8s:io.kubernetes.pod.namespace": monitoring# __EGRESS_POSTGRES_BLOCK__— if Postgres is enabled, replace with:
Otherwise delete the line.# Allow default postgres cluster - toEndpoints: - matchLabels: "k8s:io.kubernetes.pod.namespace": default cnpg.io/cluster: postgres toPorts: - ports: - port: "5432" protocol: TCP# __EGRESS_ANUBIS_DRAGONFLY_BLOCK__— if Anubis is enabled, replace with:
Otherwise delete the line.# Allow anubis dragonfly instance - toEndpoints: - matchLabels: "k8s:io.kubernetes.pod.namespace": anubis app: anubis-dragonfly toPorts: - ports: - port: "6379" protocol: TCP# __EGRESS_WORLD_BLOCK__— if exposure isexternal-plainorexternal-anubis, replace with:
For# Allow internet egress - toEntities: - worldinternalexposure, leave a commented-out version with a TODO:# # TODO: uncomment if this app needs to reach the public internet # - toEntities: # - world
Registering in apps/kustomization.yaml
Insert - <app> alphabetically into the resources: list of /var/home/wynnj/projects/home-ops/apps/kustomization.yaml. Preserve commented-out entries in place. Use Edit with a tightly scoped old_string that anchors on the two adjacent entries (one before, one after).
Final report
After writing the files, output a short summary:
- Files created (full list).
- Options selected (postgres / dragonfly / anubis / external / etc.).
- Remaining TODOs grouped by file:
helmrelease.yaml: image repo + tag, homepage description/icon/weight, port verification, env vars (anything beyond TIMEZONE/APP_PORT/APP_URL that the upstream image needs).externalsecret.yaml(if present): Bitwarden Secrets Manager key + field names must exist.netpols.yaml: review egress allow-list (especially theworldblock).pvc.yaml(if present): adjust size and mount path.
- Reminder: if the app needs its own load-balancer IP (separate
LoadBalancerService, not via envoy gateway), add aSVC_<APP_UPPER>_ADDRentry undercomponents/cluster-vars/.
Do not commit, do not run task or flux or kubectl commands. Just create the files and report.