new-app

star 0

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`.

jameswynn By jameswynn schedule Updated 6/12/2026

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 with test -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).

  1. Postgres — "Does this app need a Postgres database (the default cluster at postgres-rw.default.svc.cluster.local)?" — yes / no
  2. 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
  3. Dragonfly — "Does this app need a Dragonfly (Redis-compatible) cache via the shared component?" — yes / no
  4. Persistent volume — "Does this app need a PersistentVolumeClaim for on-disk state?" — yes / no
  5. 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:

  1. Substitute __APP__<app> (literal text replace, every occurrence).
  2. Substitute __APP_UPPER__<APP_UPPER>.
  3. 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 with components: + indented entries based on options. Order: dragonfly first, then anubis. If neither dragonfly nor anubis is enabled, delete this line entirely (no components: field).
    • Dragonfly entry: - ../../../components/dragonfly
    • Anubis entry: - ../../../components/anubis
  • # __SUBSTITUTE_EXTRAS__ — replace with anubis-related substitutions if anubis is enabled, otherwise delete the line.
    • Anubis substitutions (indented under substitute:, same indent as APP:):
            APP_NAMESPACE: *namespace
            ANUBIS_TARGET: http://<app>:<port>
      

app/kustomization.yaml

  • # __RESOURCES_EXTRAS__ — replace with the additional resource entries that apply, otherwise delete the line:

    • - externalsecret.yaml if Postgres OR other-Bitwarden-secret is enabled.
    • - pvc.yaml if persistent volume is enabled.

    Note: there is a single externalsecret.yaml file regardless of whether Postgres is enabled — it holds both ExternalSecrets when Postgres is on (see below).

app/helmrelease.yaml

  • # __INIT_CONTAINERS_BLOCK__ — if Postgres is enabled, replace with:
          initContainers:
            init-db:
              image:
                repository: ghcr.io/home-operations/postgres-init
                tag: 18.3
                pullPolicy: IfNotPresent
              envFrom:
                - secretRef:
                    name: <app>-init-db
    
    Otherwise delete the line.
  • # __DRAGONFLY_ENV_BLOCK__ — if Dragonfly is enabled, replace with:
                REDIS_HOST: <app>-dragonfly
                REDIS_PORT: 6379
    
    Otherwise delete the line.
  • # __ENVFROM_BLOCK__ — if the app has any ExternalSecret (Postgres OR other-Bitwarden), replace with:
              envFrom:
                - secretRef:
                    name: *app
    
    Otherwise delete the line.
  • __ROUTE_DOMAIN__SECRET_INTERNAL_DOMAIN for internal exposure, SECRET_EXTERNAL_DOMAIN for external exposure (with or without Anubis). Wrap as ${SECRET_INTERNAL_DOMAIN} / ${SECRET_EXTERNAL_DOMAIN} — the final line should read hostnames: [ "${APP}.${SECRET_..._DOMAIN}" ].
  • __ROUTE_PARENT__envoy-internal for internal, envoy-external for external (with or without Anubis).
  • # __ROUTE_RULES_BLOCK__ — if Anubis is enabled, replace with:
          rules:
            - backendRefs:
                - name: <app>-anubis
                  port: 8080
    
    Otherwise delete the line.
  • # __PERSISTENCE_BLOCK__ — if persistent volume is enabled, replace with:
        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
    
    Otherwise delete the line.

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 name app/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 single app/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:
      # Allow default postgres cluster
      - toEndpoints:
          - matchLabels:
              "k8s:io.kubernetes.pod.namespace": default
              cnpg.io/cluster: postgres
        toPorts:
          - ports:
              - port: "5432"
                protocol: TCP
    
    Otherwise delete the line.
  • # __EGRESS_ANUBIS_DRAGONFLY_BLOCK__ — if Anubis is enabled, replace with:
      # Allow anubis dragonfly instance
      - toEndpoints:
          - matchLabels:
              "k8s:io.kubernetes.pod.namespace": anubis
              app: anubis-dragonfly
        toPorts:
          - ports:
              - port: "6379"
                protocol: TCP
    
    Otherwise delete the line.
  • # __EGRESS_WORLD_BLOCK__ — if exposure is external-plain or external-anubis, replace with:
      # Allow internet egress
      - toEntities:
          - world
    
    For internal exposure, 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 the world block).
    • pvc.yaml (if present): adjust size and mount path.
  • Reminder: if the app needs its own load-balancer IP (separate LoadBalancer Service, not via envoy gateway), add a SVC_<APP_UPPER>_ADDR entry under components/cluster-vars/.

Do not commit, do not run task or flux or kubectl commands. Just create the files and report.

Install via CLI
npx skills add https://github.com/jameswynn/home-ops-public --skill new-app
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator