一键导入
add-resource
Add support for a Kubernetes resource type by choosing the required catalog, refresh, detail, object-map, permission, frontend, docs, and test surfaces
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Add support for a Kubernetes resource type by choosing the required catalog, refresh, detail, object-map, permission, frontend, docs, and test surfaces
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when wanting to systematically improve the React/TypeScript frontend - scans for security vulnerabilities, stability risks, performance issues, and code simplification opportunities, then presents 5 ranked findings for the user to choose from
Guide for safely modifying the refresh/streaming subsystem — covers the full domain lifecycle, registration points, and known fragility areas
Use when eliminating representable-but-invalid states in Luxury Yacht — converting boolean flag-soup to discriminated unions, making required identity fields non-optional, replacing stringly-typed states with literal/typed enums, and pushing scattered runtime guards into the type system or a single chokepoint. Triggers — "make impossible states impossible", flag soup (isLoading/isError/isEmpty), contradictory nullable fields, kind-only/name-only object refs, stringly-typed status, or the docs/todo.md item of the same name.
Work on Luxury Yacht object-panel details, YAML, actions, logs, shell/debug tabs, docked panels, related objects, and tests
Use for large-scale structural Luxury Yacht app reviews that audit broad systems or cross-cutting concerns, identify major simplification, hardening, optimization, or refactoring opportunities, and optionally write temporary phased plans in docs/plans
Work on cluster/namespace views, browse/catalog surfaces, shared GridTable behavior, large datasets, filters, and refresh-backed table tests
| name | add-resource |
| description | Add support for a Kubernetes resource type by choosing the required catalog, refresh, detail, object-map, permission, frontend, docs, and test surfaces |
Add support for a Kubernetes resource type. Resource support can span several surfaces; decide the required surfaces up front instead of assuming this is only a rich-detail task.
This skill covers rich object detail/action support. If the resource also needs
to appear in a table or refresh-driven view, add a refresh-domain payload under
backend/refresh/snapshot and wire the matching frontend refresh domain. Do not
add new list/table payloads to backend/resources; that package is the
detail/action service layer.
Before editing code, decide which surfaces the resource needs:
| Surface | Backend Entry Points | Frontend Entry Points | Notes |
|---|---|---|---|
| Discovery/catalog/browse | backend/objectcatalog, backend/refresh/snapshot/catalog.go | frontend/src/modules/browse | The object catalog owns existence, GVK/GVR, scope, namespace listings, and cluster listings. |
| Refresh table/list | backend/refresh/snapshot/*.go, backend/refresh/system/registrations.go | frontend/src/core/refresh/*, GridTable consumers | Canonical list/table data belongs in refresh snapshots. |
| Resource stream rows | backend/refresh/resourcestream | frontend/src/core/refresh/streaming | Stream row shape must match snapshot row shape. |
| Rich object details/actions | backend/resources/<kind>/ (details.go, actions.go, dto.go), generated detail bindings (go generate ./backend) | Object-panel details/overview registry | Use for detail tabs, logs/debug helpers, and imperative operations. |
| Per-kind identity/descriptor | backend/resources/<kind>/{identity,descriptor}.go, one entry in backend/kind/kindregistry, identity row in backend/resourcecontract | n/a | Every subsystem loops the registry by facet; do not name the kind elsewhere. |
| Shared identity/status/links/facts | backend/resources/<kind>/{model,facts}.go built on shared primitives in backend/resourcemodel | status/link rendering utilities | Backend owns primary status and relationship semantics. |
| YAML/edit/apply | object YAML/read/apply backend paths | object-panel YAML tab | Must carry clusterId and full GVK identity. |
| Object map | .agents/skills/object-map/SKILL.md, backend/resources/<kind>/objectmap*.go | frontend/src/modules/object-map, object-panel support list | Fix backend graph data before frontend renderer/allowlist changes. |
| Permissions/capabilities | refresh permission checks, capability backends | RBAC-gated UI/action surfaces | Keep permission-denied diagnostics visible. |
| Docs/tests | owning architecture/workflow docs | adjacent specs/stories when useful | Update durable docs when contracts or supported kinds change. |
If the requested task only needs one surface, keep the implementation scoped to that surface. If user-visible support would be incomplete without another surface, explain the tradeoff before narrowing.
/add-resource <Kind> — e.g., /add-resource CronJob, /add-resource Ingress
backend/resources/<kind>/ (e.g. deployment, service, configmap). For rich object details/actions, create backend/resources/<kind>/ (or extend it if it already exists). For table/list data, use backend/refresh/snapshot instead.backend/resources/deployment/ is the canonical example (identity.go, descriptor.go, appbinding.go, model.go, facts.go, dto.go, details.go, actions.go, object-map files). Pick a neighbour that matches your resource's shape. Shared cross-kind helpers live in backend/resources/workloads, backend/resources/common, and backend/resources/types.docs/architecture/shared-resource-model.md before adding status, relationship links, capability checks, or object references. The backend owns status semantics; frontend status classes come from statusPresentation; relationship links use resourcemodel.ResourceLink; object references must carry clusterId, group, version, kind, and concrete object names.
Do not guess resource from kind, and do not treat an empty Kubernetes
apiVersion as core v1.docs/architecture/refresh-system.md, docs/architecture/data-access.md,
and .agents/context/code-map.md.Directory: backend/resources/<kind>/
A kind defines itself once in its own package and is registered once. Create
these files (see backend/resources/deployment/ for the full shape):
identity.go — var Identity = resourcekind.Identity{Group, Version, Kind, Resource, Namespaced}. This is the single source of the kind's GVK identity (resourcekind is a dependency-free leaf).descriptor.go — var Descriptor = kindspec.Descriptor{Identity, CatalogSource, DetailCacheable, Stream, Collector, Edges, Binding, Graph, Workload, PortForward}. Leave any facet nil/zero when the kind does not participate in that subsystem.model.go — BuildResourceModel(clusterID, obj), BuildFacts(obj), and BuildStatusPresentation(obj), built from the shared primitives in backend/resourcemodel (e.g. WorkloadResourceModel, WorkloadCommonFacts, ConditionFacts, ResourceLink). Do not re-add per-kind files to backend/resourcemodel; it now holds only shared primitives + the relationship index.facts.go — the typed per-kind Facts struct. Keep facts semantic; do not add empty slots to reserve future space.Then register the kind in exactly one place:
<kind>.Descriptor — to var All in backend/kind/kindregistry/registry.go.Every subsystem (object catalog, resource-stream, snapshot stream-summary,
object-map, detail bindings, response-cache invalidation) loops that registry and
filters by facet. If a dispatch path forces you to name the kind elsewhere, fix
the generic mechanism instead of adding a special case. See
docs/architecture/resource-kind-registry.md.
clusterId, group, version, kind,
resource, scope, namespace, and name.ResourceLink; use display-only refs only when
the source does not provide enough identity for safe navigation.File: backend/resources/<kind>/details.go
Follow this pattern:
package <kind>
import (
"github.com/luxury-yacht/app/backend/resources/common"
// k8s API imports for the resource
)
// Service provides detailed <Kind> views backed by shared dependencies.
type Service struct {
deps common.Dependencies
}
// NewService constructs a <Kind> service using the supplied dependencies bundle.
func NewService(deps common.Dependencies) *Service {
return &Service{deps: deps}
}
// <Kind> returns the detailed view for a single resource.
func (s *Service) <Kind>(namespace, name string) (*<Kind>Details, error) {
client := s.deps.KubernetesClient
if client == nil {
return nil, fmt.Errorf("kubernetes client not initialized")
}
item, err := client.<APIGroup>().<Resources>(namespace).Get(s.deps.Context, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get <kind>: %w", err)
}
model := BuildResourceModel(s.deps.ClusterID, item)
facts := BuildFacts(item)
// Fetch related resources, project the model's status/facts into the DTO,
// and return display-ready details.
}
Key points:
common.Dependencies — never construct clients directly(namespace, name string)(name string) onlys.deps.MetricsClient when the resource manages podss.deps.Context and s.deps.ClusterID; do not use
context.Background() or unscoped identity in resource servicesmodel.go/facts.go
(built on the shared backend/resourcemodel primitives)File: backend/resources/<kind>/dto.go
Add a <Kind>Details struct (the frontend wire shape, co-located with the kind's
model and detail builder). Include:
restypes.StatusProjection (it carries status, statusState,
statusPresentation, and statusReason).backend/resources/types (e.g. restypes.PodSimpleInfo)Look at backend/resources/deployment/dto.go for field naming conventions — DTOs
use plain strings for display values, not raw Kubernetes types. Shared cross-kind
field types stay in backend/resources/types; only the <Kind>Details struct
itself lives in the kind package.
If Go DTOs change, refresh or verify the Wails bindings in
frontend/wailsjs/go/models.ts. wails generate may not work in every local
run, so validate bindings with frontend typecheck.
File: backend/resources/<kind>/appbinding.go
The App.Get<Kind> wrapper and the object-panel detail-fetcher dispatch map are
generated; you declare one binding spec, then regenerate. Do not hand-edit
objectDetailFetchers in backend/object_detail_provider.go or the generated
resource_details_generated.go / object_detail_fetchers_generated.go.
package <kind>
import "github.com/luxury-yacht/app/backend/resources/appbinding"
// DetailBinding declares this kind's App.Get binding for the genappbindings generator.
var DetailBinding = appbinding.Spec{
Identity: Identity,
Service: "<kind>.NewService(deps)",
Import: "github.com/luxury-yacht/app/backend/resources/<kind>",
}
Reference it from the kind's Descriptor (Binding: &DetailBinding), then run
go generate ./backend (see backend/generate.go) to regenerate the wrappers and
the dispatch map. The generated objectDetailFetcherGVKs (derived from the binding
plus resourcecontract.BuiltinResources) is the exact-GVK gate that keeps a custom
resource with a colliding built-in kind from being served by the wrong typed
fetcher — you no longer maintain it by hand.
Do not add per-kind raw-object fallbacks in
backend/refresh/snapshot/object_details.go. That snapshot builder delegates
rich detail resolution to the app-level ObjectDetailProvider and already
falls back to a generic details payload for unsupported or custom kinds.
Files:
frontend/wailsjs/go/models.tsfrontend/src/modules/object-panel/components/ObjectPanel/Details/detailsTabTypes.tsfrontend/src/modules/object-panel/components/ObjectPanel/ObjectPanel.tsxfrontend/src/modules/object-panel/components/ObjectPanel/Details/DetailsTab.tsxfrontend/src/modules/object-panel/components/ObjectPanel/Details/useOverviewData.tsAdd the new details type to DetailsTabProps and thread it through:
<kind>Details: types.<Kind>Details | null;
EMPTY_DETAILS slot and detailPayload switch case in
ObjectPanel.tsx.DetailsTab.tsx.UseOverviewDataParams and map it into the overview shape in
useOverviewData.ts.Files: backend/resourcecontract/builtin_resources.go,
backend/resourcecontract/builtin-resource-identities.json
The kind's identity comes from its identity.go (Section 1). Aggregate it into
the authoritative built-in table by adding fromIdentity(<kind>.Identity) to
var BuiltinResources in builtin_resources.go, and add the matching row to
builtin-resource-identities.json (a drift test enforces that the two agree).
backend/objectcatalog/identity.go's builtinResourceCatalog is derived from
resourcecontract.BuiltinResources, so it picks up the new kind automatically —
do not hand-maintain a second identity table there. Do not add custom resources
to the contract; CRDs hydrate through discovery/CRD data and carry their real
group/version.
The shared interface in backend/resources/common/resource_identity.go should
remain only a contract. Do not add another resolver table or kind-only fallback
there.
File: frontend/src/shared/constants/builtinGroupVersions.ts
If this is a built-in Kubernetes kind with a first-class frontend view, add its canonical group/version to the built-in lookup. Do not add custom resources here; custom resources must carry group/version from catalog or API data.
Directory: frontend/src/modules/object-panel/components/ObjectPanel/Details/Overview/
If the resource is similar to an existing kind (e.g., another workload), extend the existing component with conditional rendering:
const is<Kind> = normalizedKind.toLowerCase() === '<kind-lowercase>';
{is<Kind> && (
<OverviewItem label="SomeField" value={someValue} />
)}
If the resource is substantially different, create a new <Kind>Overview.tsx component following the same prop pattern as WorkloadOverview.tsx.
File: frontend/src/modules/object-panel/components/ObjectPanel/Details/Overview/registry.ts
Register the overview renderer for the new kind:
overviewRegistry.register({
kinds: ['<kind-lowercase>'],
component: <OverviewComponent>,
mapProps: (props) => ({ <kind>Details: props.<kind>Details || props }),
});
Do not rely on registry capabilities as the source of truth for object-panel
actions or tabs; current feature support is driven by RESOURCE_CAPABILITIES.
File: frontend/src/modules/object-panel/components/ObjectPanel/constants.ts
Add or update RESOURCE_CAPABILITIES for supported object-panel actions and
tabs:
delete for deletable resourcesrestart only for restartable workloadsscale only for scalable workloadsobjPanelLogs, shell, debug, trigger, suspend, or nodeLogs only
when the workflow is implemented for that kindPermission checks are evaluated from the panel object's clusterId,
group/version, kind, namespace, and name in
frontend/src/modules/object-panel/components/ObjectPanel/hooks/useObjectPanelCapabilities.ts.
If a new action kind needs different verbs, subresources, or target resources,
update that hook and the backend permission/action path together.
If the resource appears in a table/list refresh surface, update the refresh contract together:
backend/refresh/snapshot/*.gobackend/refresh/system/registrations.gobackend/refresh/snapshot/permission.go and the
gate in backend/refresh/system/permission_gate.goStream descriptor facet;
per-domain snapshot builders loop kindregistry.StreamDescriptorsForDomain and
collect rows via collectDescriptorTableRows in
backend/refresh/snapshot/stream_collectors.go (shared row helpers in
streaming_helpers.go)Stream *streamspec.Descriptor on
the kind's Descriptor (row DTO in backend/kind/streamrows); it is registered
automatically by registerDescriptorStreams in
backend/refresh/resourcestream/stream_descriptor_dispatch.go. Add a bespoke
handler in stream_registration_direct.go/stream_registration_network.go only
for related-object invalidation or a non-shared informer factory. Add resource
stream tests when live row updates are neededbackend/internal/genrefreshcontracts/registry.go; run
go generate ./backend and never hand-edit
frontend/src/core/refresh/types.generated.tsfrontend/src/core/refresh/streaming/resourceStreamDomains.ts when live row
updates are neededfrontend/src/core/refreshRefresh domains are single-cluster only, including Resource WebSocket domains. Do not add multi-cluster descriptor flags or send multi-cluster scopes to snapshot, manual refresh, or stream paths; background refresh should fan out as separate single-cluster requests.
For larger table/list work, use .agents/skills/browse-tables/SKILL.md and
.agents/skills/refresh-subsystem/SKILL.md alongside this skill.
Files: backend/resources/<kind>/details_test.go (service/DTO) and
backend/resources/<kind>/model_test.go (model/facts/status)
Follow the established test pattern:
func TestService<Kind>(t *testing.T) {
// 1. Create fixtures
resource := &<apiType>{...}
// 2. Create fake client
client := cgofake.NewClientset(resource)
// 3. Create deps with testsupport helpers
deps := testsupport.NewResourceDependencies(
testsupport.WithDepsContext(context.Background()),
testsupport.WithDepsKubeClient(client),
)
// 4. Instantiate service and call method
service := NewService(deps)
details, err := service.<Kind>("namespace", "name")
// 5. Assert
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Assert specific fields...
}
Check backend/testsupport/ for available fixture helpers and option functions.
File: backend/objectcatalog/service.go
If the resource should appear quickly in the catalog, add it to streamingResourcePriority. Lower numbers = higher priority. Most resources don't need this — only add it if the resource is commonly viewed.
Before marking done:
identity.go + descriptor.go and registered with one entry in backend/kind/kindregistryresourcemodel primitives) or explicitly deemed unnecessaryService (NewService) fetches the resource and related resources<Kind>Details DTO defined in backend/resources/<kind>/dto.go with display-ready fieldsstatusPresentationresourcemodel.ResourceLink constructors and are validatedappbinding.Spec declared (Binding on the descriptor) and go generate ./backend re-run (generated detail dispatch + exact-GVK gate)ObjectPanel.tsx, DetailsTabProps, DetailsTab.tsx, and useOverviewData.tsresourcecontract.BuiltinResources and builtin-resource-identities.jsonRESOURCE_CAPABILITIES reflects supported object-panel actions/tabsmage qc:prerelease passesUse focused checks while iterating:
go generate ./backend # regenerate detail bindings after adding/changing a kind
go test ./backend ./backend/resources/... ./backend/resourcemodel ./backend/kind/... ./backend/refresh/snapshot
npm run typecheck --prefix frontend
npm run test --prefix frontend -- <relevant spec or module>
Then run the final gate for non-documentation work:
mage qc:prerelease
git diff --check
git status --short
Because mage qc:prerelease runs frontend lint-fix, inspect the worktree after
it completes.