About wi-app
wi-app is an agent skill for working on wonder-image/app as a Composer framework package rather than a standalone site. It helps developers place changes in the framework, a site, or an external module and follow the package’s architecture and security conventions.
Key features
- 01Distinguishes framework, site, and module work
- 02Maps tasks to architecture references
- 03Documents model and resource extension rules
- 04Covers API, CSRF, and security conventions
Use cases
- 01Classifying a requested change before editing
- 02Finding the relevant framework reference for a task
- 03Checking placement and validation rules for a component or API change
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ wonder-image/ skills --skill wi-appAfter installing, type/wi-appin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/architecture.mdcomponent-catalogue.mdmodel-and-resource.mdpermissions-and-users.mdscheduler.mdsecurity.mdworkflows.mdDetails
From the project READMEWonder Image App
Overview
Use this skill to work inside wonder-image/app as a framework package, not as a standalone app. Keep the framework-versus-site boundary explicit before changing code, running commands, or deciding where new logic belongs.
Glossary
Shared vocabulary across wi-app and wi-site (identical wording in both skills):
- framework —
wonder-image/app(Composer package, github.com/wonder-image/app). Lives at the repo root when wi-app is active; lives atvendor/wonder-image/app/inside a site. - lib —
wonder-image/lib(npm package, github.com/wonder-image/lib). The JS / CSS design system. Source atnode_modules/wonder-image/src/; compiled copy atassets/lib/wonder-image/dist/. - scaffold —
wonder-image/new-site(github.com/wonder-image/new-site). The site template. The scaffold itself is also a valid site. - site — any project whose
composer.jsondepends onwonder-image/app(i.e. installs the framework undervendor/wonder-image/app). Typically the scaffold or a project derived from it. Canonical term — prefer "site" over older mixed uses like "consumer project" / "consumer app". - module — external Composer package
wonder-image/<slug>discovered via Composer (or filesystem fallback). Ships its own models / resources / routes viamodule.json+ aModuleInterfaceentrypoint. custom/— site-only directory for project pages, components, layouts, routes, config, and helpers. Never present inside the framework.app/Models/app/Resources— site PSR-4 roots (App\Models\.../App\Resources\...). Distinct from the framework'sclass/App/Models/class/App/Resources(Wonder\App\Models\.../Wonder\App\Resources\...).
When to switch to wi-site
Switch to wi-site when any of these signals are true — the work belongs in a site repo, not in the framework:
- the repo's
composer.jsondoes not declare"name": "wonder-image/app"; instead it listswonder-image/appunderrequire(the framework is installed as a dependency). - a
docs/ai/PRODUCT.mdexists (or, on sites not yet migrated, aPRODUCT.mdat the repo root) declaring a Wonder site (site_type: landing/corporate/blog/ecom/rsvp) — that file only exists on sites, never inside the framework itself. vendor/wonder-image/app/exists at the repo root — the framework is installed undervendor/, so this is a site.- the requested change lives under
custom/,app/Models,app/Resources,lang/,assets/{ASSETS_VERSION}/, or any path that does not exist inside the framework repo. - the task is "add a page", "translate strings", "register a permission key for this site", "edit color tokens", "use a
.wi-*component / lib class", or any work that uses (rather than extends) the framework. - you are about to patch a file under
vendor/wonder-image/app/. Do not. Either reshape the change to live incustom//app/(still wi-site), or open the change against the framework repo itself (then wi-app applies there).
Start Here
- Classify the request before editing anything.
- Decide whether the change belongs to the framework, a site, or an external module package.
- Read only the reference file that matches the task:
references/architecture.mdfor bootstrap, registries, routes, env resolution, or module loadingreferences/model-and-resource.mdfor the Model / Resource extension API, schemas, repeater fields, andCustomPageSchemareferences/permissions-and-users.mdfor the permission builder API, permission keys, and shared user-management resourcesreferences/workflows.mdfor change placement, validation, and common gotchasreferences/security.mdfor CSRF, cookies, auth helpers, uploads, redirects, exports, SQL escaping, error/log hygiene, backups and consent
Core Rules
- Treat
wonder-image/appas a Composer library. The real runtime lives in the site that installs it undervendor/wonder-image/app. - Assume
php forge ...commands are site-side commands unless you confirm otherwise. Do not expect aforgeexecutable in this package root. - Prefer
class/App/*for new logic. Touch legacyapp/*only when the runtime still depends on it. - API handlers live under
app/http/api/*. Do not reintroduce package handlers underapp/api/*; call routed/api/...endpoints viaPath::$api,Path::$appApi, andPath::$apiDT. - API bearers are verified by
Wonder\Api\Token\BearerVerifier(signature,exp,hash_equalsagainstapi_users.token, authority, IP/domain); reuse it instead of decoding JWTs by hand. Mint withApiTokens::issue(), rotate withregenerateApiToken(). Never print a bearer in HTML (Credentials::appToken(),ApiTokens::pageToken()): pages printconst API_TOKEN = '';and the lib omits an emptyAuthorization. A request without bearer to an endpoint that allows page tokens is@systempage access whenEndpoint::sessionRequestAllowed()holds (same origin, active session,Csrf::verify()for non-GET/HEAD) andBearerVerifier::verifySession()passes;isPageToken()is true and row visibility uses the session user. To renew after expiry, a site page fetches a new CSRF (/api/csrf/) and updates_csrfand<meta name="wi-csrf">;/api/token/page/stays only for compatibility. Server-only endpoints (deploy, cron, external triggers) passallowPageToken: falsetoHandler::run().api_activitykeeps onlyApiTokens::fingerprint(). Seedocs/app/piattaforma/api.md#token. - Keep runtime fallbacks and seed payloads separate:
Wonder\\App\\RuntimeDefaultsis for runtime defaults used while rendering or composing in-memory config.Wonder\\App\\SeedDefaultsis for idempotent bootstrap/seed payloads consumed bybuild/row, setup commands, and seed-backed singleton forms.
- Program in terms of future integration and extension. Prefer class and function designs that can be reused, extended, overridden, or integrated by sites and external modules instead of solving only the immediate local case.
- Keep the architecture split clear:
Model::tableSchema()defines SQL structureModel::dataSchema()defines data preparation and persistence behaviorResource::formSchema()defines backend inputsCustomPageSchemahandles non-CRUD backend pagesRepeaterhandles repeatable rows and related-row sync
- Form inputs go through the
FormFieldhierarchy — always. Every input on the frontend Wonder theme (class/Themes/Wonder/) and the backend Bootstrap theme (class/Themes/Bootstrap/) is declared viaFormField/RepeaterColumn(or directly through a typedInputs\Input*) and rendered throughInput::render($theme). No raw<input>/<select>/<textarea>HTML, no ad-hoc render functions. Missing input types are added at the framework layer (typedInputs\Input*withelement()→ helper onFormField→ renderer under each theme), not patched at the call site. Full rationale and the four-step "missing type" workflow inreferences/model-and-resource.md. - Wonder date renderers describe
DatePicker,DateRange,DateTimeRange, andSelectDatewith escapeddata-wi-*attributes only. Their jQuery UI initialization belongs towonder-image/lib/src/build/frontend/js/form/input.jsthroughsetInput(); never interpolate field values or bounds into renderer-owned inline scripts. - Translations go through
Wonder\Localization: the Set-up Traduzioni page exports/imports aCatalogofTranslationSources (json,legal,model:<table>; modules add sources withlocalization.sources, sites withCatalog::set(),falseremoves); imports are previewed, stored server-side and never delete a key. Translatable Models implementTranslatable+HasTranslationsand read withModel::translated($rows, $lang); values live intranslationswith statusautomatic/reviewed/review, and onlywonder.content-translationwritesautomaticones (vocabulary, memory,Translatordriver), never overwritingreviewed.legal_documentsis the only legal source (LegalDocument::make($type)). Tests inject store and driver (Translations::useStore()), never call Google. Seedocs/app/concetti/traduzioni.md. <wi-card>is retired. Do not add new<wi-card>tags and do not callwiCard()/wiCardLink()in new code: useCardin app, which renders a<div class="card">. When you edit a page that contains one, replace it. Do not add an option toCardto change its tag. LeavecreateCard()and thewi-cardCSS rule in the lib: existing pages still depend on them until they are migrated.- The session CSRF token belongs to
Wonder\Http\Csrf(token(),field(),verify(); field_csrf, headerX-WI-CSRF, sent by the lib fromwonder-image≥ 2.1.2-alpha.31,<meta name="wi-csrf">in the base layouts).Formrenderers of both themes,Button::post(), POSTDropdownitems,Modal::form()and the Resource form page emit the field on their own throughCsrf::fieldFor(), never for GET and never without an active session; hand-written<form>tags addCsrf::field()themselves. Verification is per route: declare->csrf()on the route or group andRouteDispatcherrunsWonder\Http\CsrfGuardafterauthorizeUser()(403 on a missing or wrong token); every/api/backend/*route exceptalertalready has it, Bearer-only routes never do. Page handlers that write on POST callCsrf::verify()before writing and fail with alert929, never with a text containing "CSRF" or "token" (helpersCsrf::ALERT,failureMessage(),oldInput(),flashFailure(),renderCsrfFailure()). Never act on GET.AuthSessiondelegates toCsrf; do not add new per-feature tokens (_contact_csrfwas removed from the customer-account module, whose forms use_csrf;Impersonationkeeps its scoped token). Details and the other security contracts:references/security.md. - Confirmations on shared components use the lib contract
data-wi-confirm*(wi.confirm(), which returnsPromise<boolean>), neverwindow.confirmor hand-writtenonclick(fromwonder-imagelib ≥ 2.1.2-alpha.31). In the lib, the backendmodal(text, link, …)is a@deprecatedalias ofwi.confirm(), while the frontendmodal(selector)is@deprecatedin favor ofwi.modal.open(id, { title })/wi.modal.close(id). The core PHPmodal()(app/function/backend/modal.php) and theonclick="modal(...)"inplugin.phpandBackend\Table\Fieldare removed on main (breaking app#129, not in a tag yet: v2.4.0-beta.5 still has them); legacy Delete buttons now printdata-wi-confirm*and callajaxRequest. New code never callsmodal()or targets#modal. UseButton::confirm($message, title:, ok:, variant:)and Dropdown items withconfirm,confirm_title,confirm_ok,confirm_variant. POST actions go on the<form>(Themes\Concerns\RendersPostForm, which also prints the CSRF field), the rest on the tag. ADropdownitem with'method' => 'post'is a per-item CSRF form;action()is a<button type="button">without href.Modal::form($action, $method, $hidden)wraps body and footer in a CSRF form with Cancel then Save (cancel(),submit(); a manualfooter()replaces them) and must be rendered outside any form (it throwsLogicExceptioninside a Resource layout); rendered standalone in Bootstrap,columns()/columnSpan()lay out its fields as in a Resource layout. Date inputs read a slashed value asd/m/Y. Inner parts get one public method each (Modal::dialogClass()..footerClass(),Dropdown::toggleClass(),menuClass(),itemClass()) built onHasPartAttributes/RendersPartAttributes; classes append to the theme ones in a singleclassattribute. Seedocs/app/concetti/componenti/README.md. - A new component or public component API is not complete without its card in
docs/components/<category>/<slug>.php;php tests/Docs/CatalogRenderTest.phpmust pass. Theme availability is computed byThemeSupport, never written by hand. Seereferences/component-catalogue.md. - Update
docs/app/*in the same work when you change bootstrap, architecture, routing, layout structure, or developer-facing conventions.
Task Routing
Bootstrap or runtime
Read references/architecture.md, then inspect wonder-image.php, class/App/Credentials.php, route config, and the relevant registries. Be careful with anything that resolves ROOT, loads .env, or changes early bootstrap order.
CRUD resources, backend pages, or API generation
Read references/model-and-resource.md first for the Model / Resource extension contract, then references/architecture.md for registry precedence and route emission, then references/permissions-and-users.md for the permission keys referenced from Resource::permissionSchema().
Form inputs (frontend Wonder or backend Bootstrap)
Read references/model-and-resource.md — section "FormField hard rule". Every input on either theme is declared via FormField::key(...) / RepeaterColumn::key(...) or directly through a typed Inputs\Input*, then rendered through Input::render($theme). New input types are added under class/App/ResourceSchema/Inputs/, implement their own element(), expose a helper on FormField, and use renderers under class/Themes/Wonder/ and class/Themes/Bootstrap/ — not bespoke HTML at the call site.
UI / styling inside default components or themes
When the change touches a default component shipped by the framework (class/App/Resources/.../*.php views, the non-form Components under class/Elements/Components/, Media under class/Elements/Media/, the form Components under class/Elements/Form/Components/, the Wonder / Bootstrap renderers under class/Themes/{Wonder,Bootstrap}/, or any app/view/... page used by a site), the authoritative UI rulebook is wi-site/references/style-and-lib.md. The same reuse-first-from-wonder-image/lib policy applies inside wonder-image/app: do not invent new .wi-* names at framework level (that is a lib-side change), do not bake site-specific tokens into a default component, and preserve compatibility with the site's color.css / root.css. This includes shared action primitives like Button, Badge, ButtonGroup, and Dropdown. For link-like attributes on shared components, prefer the common concern class/Elements/Concerns/HasLinkAttributes.php and persist href, target, rel, title, onclick, download in attributes so theme renderers stay thin. Install wi-site alongside wi-app so this reference resolves locally.
Component cards and the catalogue
Read references/component-catalogue.md when adding or changing an Element in class/Elements/* or its public API: card location and format, validation test, how to open the catalogue, PageAssets::once() and the lib version constraint.
Security-sensitive code
Read references/security.md before touching request handling, sessions, cookies, uploads, exports, redirects, SQL helpers, logging or API auth. It names the owner class of each contract; reuse it instead of re-implementing the check in a handler or module.
Permissions, roles, or user management
Read references/permissions-and-users.md. Covers the builder API (Permissions::reset(), Area::make(), Permission::make()), the merge with module permissions, the runtime $PERMITS export, and the shared UserManagementResource / BackendUserResource / ApiUserResource flow.
Module system
Read references/architecture.md. Preserve compatibility with Composer discovery, filesystem fallback, enabled-module state, and route/model/resource registration from external packages.
Console commands or local-start flows
For scheduled tasks, cron entrypoints, task providers, retention, metrics, sitemap
migration or the Google Places location sync (GoogleReviews), also read references/scheduler.md.
Read references/workflows.md. Remember these commands are meant to be executed from a site even when their source lives in this package.
Validation
- Lint every touched PHP file with
php -l. - Run
composer dumpautoloadwhen classes move or new classes are added. - If the change affects bootstrap, routing, console commands, resources, models, or request handlers, validate from a site with
php forge update --localandphp forge start. - If the change is Herd-specific, also validate the local routing path described in
references/workflows.md.
Output Expectations
- Explain whether the fix belongs in the framework, a site, or a module package.
- Name the main files and registries involved instead of describing the framework generically.
- Call out integration validation needs whenever a framework-root edit only becomes real through a site.