diff --git a/build/extension.mjs b/build/extension.mjs index 995cdfa..9e37d70 100644 --- a/build/extension.mjs +++ b/build/extension.mjs @@ -29,6 +29,11 @@ async function main() { entryPoints: ["src/extension.ts", "src/test/**/*.test.ts"], bundle: true, format: "cjs", + /* Preserve original function/class names through bundling (and + * minification). The AWS SDK command classes are dispatched on by + * `constructor.name` (in tests, via a fake client), which esbuild would + * otherwise rename on identifier collisions (e.g. `DescribeKeyCommand2`). */ + keepNames: true, minify: production, sourcemap: !production, sourcesContent: false, diff --git a/build/generate-detail-fields.mjs b/build/generate-detail-fields.mjs new file mode 100644 index 0000000..3306aad --- /dev/null +++ b/build/generate-detail-fields.mjs @@ -0,0 +1,167 @@ +/** + * Detail-field generator (dev-time authoring aid, on-demand only). + * + * Produces a first-cut `detail: [{ label, path, type }]` spec for a resource + * type by reading that resource's Describe/Get (or list-item) output shape from + * an OFFLINE AWS API model — the `aws-sdk` v2 `apis/-*.normal.json` + * models, or an equivalent botocore `service-2.json`. The model source is used + * only by this generator and is never bundled into the extension. + * + * The emitted spec is a STARTING POINT: it is committed into the service + * definition and hand-refined. Runtime renders the committed, fixed subset (not + * a raw response dump). Services whose detail spans multiple calls, or whose + * only data is the list item, get a partial first cut and are finished by hand. + * + * node build/generate-detail-fields.mjs + * + * Prints a TypeScript `detail` array to stdout for pasting into a definition. + */ +/* This dev-time generator walks untyped AWS API model JSON (JSON.parse → any), + * so the type-aware "unsafe" rules add only noise here. */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return */ +import fs from "node:fs"; + +/* FieldType enum values, mirrored from src/.../serviceProvider.ts (the runtime + * enum can't be imported into a plain .mjs). Keyed by enum member name so we + * can emit `FieldType.NAME` identifiers. */ +export const FIELD_TYPE = { + NAME: "name", + ARN: "arn", + DATE: "date", + SHORT_TEXT: "shortText", + LONG_TEXT: "longText", + JSON: "json", + NUMBER: "number", + LOG_GROUP: "logGroup", +}; + +/** Members never worth showing. */ +const EXCLUDED_NAMES = new Set([ + "ResponseMetadata", + "NextToken", + "NextMarker", + "Marker", + "nextToken", + "MaxResults", + "MaxItems", +]); + +const PAGINATION_SUFFIXES = ["Token", "Marker"]; + +/** + * Map an API member (its name + resolved shape type) to a FieldType enum member + * name. `shapeType` is the botocore/aws-sdk shape `type` + * (string|integer|long|float|double|boolean|timestamp|structure|list|map|blob). + */ +export function mapFieldType(name, shapeType) { + if (shapeType === "timestamp") return "DATE"; + if (["integer", "long", "float", "double"].includes(shapeType)) { + return "NUMBER"; + } + if (["structure", "list", "map"].includes(shapeType)) return "JSON"; + /* strings (and anything else) default to NAME, with a couple of refinements */ + if (/Arn$/.test(name)) return "ARN"; + if (/LogGroup/i.test(name)) return "LOG_GROUP"; + return "NAME"; +} + +/** Importance bucket (lower = more important) for a member name + shape type. */ +export function importanceRank(name, shapeType) { + if (/Name$/.test(name) || /Id$/.test(name) || /Arn$/.test(name)) { + return 0; /* identifiers / names */ + } + if (/(Status|State)$/.test(name)) return 1; + if (/(Type|Mode)$/.test(name)) return 2; + if (shapeType === "timestamp" || /(Time|Date|Timestamp)$/.test(name)) { + return 3; /* timestamps */ + } + if (["structure", "list", "map", "blob"].includes(shapeType)) { + return 5; /* nested / blobs sink to the bottom */ + } + return 4; /* other top-level scalars */ +} + +/** Convert a PascalCase/camelCase member name into a spaced display label. */ +export function toLabel(name) { + return name + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") + .replace(/^./, (c) => c.toUpperCase()); +} + +/** + * Rank, filter, and cap a list of `{ name, shapeType }` members into ordered + * FieldSpec-like entries `{ label, path, typeName }` (typeName is a FIELD_TYPE + * member name). Excludes metadata/pagination/blobs; collapses nested shapes to + * JSON; caps at `cap` (default 12). + */ +export function rankAndSelect(members, cap = 12) { + const kept = members.filter(({ name, shapeType }) => { + if (EXCLUDED_NAMES.has(name)) return false; + if (PAGINATION_SUFFIXES.some((s) => name.endsWith(s))) return false; + if (shapeType === "blob") return false; + return true; + }); + + kept.sort((a, b) => { + const ra = importanceRank(a.name, a.shapeType); + const rb = importanceRank(b.name, b.shapeType); + if (ra !== rb) return ra - rb; + return a.name.localeCompare(b.name); /* stable, deterministic */ + }); + + return kept.slice(0, cap).map(({ name, shapeType }) => ({ + label: toLabel(name), + path: name, + typeName: mapFieldType(name, shapeType), + })); +} + +/** + * Resolve the top-level output members of an operation from an aws-sdk v2 + * `.normal.json` model, as `{ name, shapeType }[]`. + */ +export function resolveOutputMembers(model, operationName) { + const op = model.operations?.[operationName]; + if (!op) { + throw new Error(`Operation not found in model: ${operationName}`); + } + const outputShapeName = op.output?.shape; + if (!outputShapeName) return []; + const outputShape = model.shapes?.[outputShapeName]; + const members = outputShape?.members ?? {}; + return Object.entries(members).map(([name, ref]) => { + const memberShape = model.shapes?.[ref.shape]; + return { name, shapeType: memberShape?.type ?? "string" }; + }); +} + +/** Render the selected entries as a pasteable TypeScript `detail` array. */ +export function renderDetailArray(entries) { + const lines = entries.map( + ({ label, path, typeName }) => + `\t\t\t\t{ label: ${JSON.stringify(label)}, path: ${JSON.stringify( + path, + )}, type: FieldType.${typeName} },`, + ); + return `detail: [\n${lines.join("\n")}\n\t\t\t],`; +} + +function main() { + const [modelPath, operationName] = process.argv.slice(2); + if (!modelPath || !operationName) { + console.error( + "Usage: node build/generate-detail-fields.mjs ", + ); + process.exit(1); + } + const model = JSON.parse(fs.readFileSync(modelPath, "utf-8")); + const members = resolveOutputMembers(model, operationName); + const entries = rankAndSelect(members); + console.log(renderDetailArray(entries)); +} + +/* Run only when invoked directly, so the pure functions stay importable. */ +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/build/generate-service-manifest.mjs b/build/generate-service-manifest.mjs new file mode 100644 index 0000000..afbcdb2 --- /dev/null +++ b/build/generate-service-manifest.mjs @@ -0,0 +1,116 @@ +/** + * Service-manifest generator (dev-time, on-demand only). + * + * Reads LocalStack's published coverage data and emits + * `resources/service-manifest.json` — the static, single-source-of-truth list + * of every service the resource browser knows about. Each manifest entry is + * `{ id, name }` where `id` is AWS's own service code (the SDK/endpoint id, + * e.g. `s3`, `logs`, `states`) and `name` is a human display name. Every + * service in the coverage data is included; community/pro availability is + * deliberately neither read nor stored (the browser also targets real AWS, so + * every service is treated as fully available). + * + * Source: the `localstack-docs` repo's coverage data + * (`src/data/coverage/*.json` + `service_display_name.json`). This generator + * is NOT run at build time and the docs repo is NOT a build dependency — it is + * run by hand when a developer notices the manifest is out of date: + * + * node build/generate-service-manifest.mjs [path-to-localstack-docs] + * + * The docs path defaults to a sibling `../localstack-docs` checkout and can be + * overridden by the first CLI argument or the LOCALSTACK_DOCS_PATH env var. + * The emitted JSON is committed to the repo so the extension build needs no + * network and no sibling checkout. + */ +/* This dev-time generator walks untyped coverage JSON (JSON.parse → any), so + * the type-aware "unsafe" rules add only noise here. */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return */ +import fs from "node:fs"; +import path from "node:path"; + +/* Coverage service-slug -> AWS service code, where they differ. The coverage + * data keys Step Functions as `stepfunctions`, but its AWS service code (SDK + * endpoint id, and the id the provider registers under) is `states`. */ +const SERVICE_CODE_OVERRIDES = { + stepfunctions: "states", +}; + +/* Coverage files that are not services (lookup tables etc.). */ +const NON_SERVICE_FILES = new Set(["service_display_name.json"]); + +function resolveDocsRoot() { + const fromArg = process.argv[2]; + const fromEnv = process.env.LOCALSTACK_DOCS_PATH; + if (fromArg) return path.resolve(fromArg); + if (fromEnv) return path.resolve(fromEnv); + /* default: sibling checkout next to this repo */ + return path.resolve(import.meta.dirname, "..", "..", "localstack-docs"); +} + +/** Title-case a service slug as a last-resort display name. */ +function titleCaseSlug(slug) { + return slug + .split(/[-_]/) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +function main() { + const docsRoot = resolveDocsRoot(); + const coverageDir = path.join(docsRoot, "src", "data", "coverage"); + if (!fs.existsSync(coverageDir)) { + console.error( + `Coverage data not found at ${coverageDir}.\n` + + `Pass the localstack-docs path as the first argument or set ` + + `LOCALSTACK_DOCS_PATH.`, + ); + process.exit(1); + } + + const displayNamePath = path.join(coverageDir, "service_display_name.json"); + const displayNames = fs.existsSync(displayNamePath) + ? JSON.parse(fs.readFileSync(displayNamePath, "utf-8")) + : {}; + + const entries = []; + const seen = new Set(); + for (const file of fs.readdirSync(coverageDir)) { + if (!file.endsWith(".json") || NON_SERVICE_FILES.has(file)) continue; + const data = JSON.parse( + fs.readFileSync(path.join(coverageDir, file), "utf-8"), + ); + const slug = data.service; + if (!slug) continue; /* skip malformed / non-service entries */ + + const id = SERVICE_CODE_OVERRIDES[slug] ?? slug; + if (seen.has(id)) continue; + seen.add(id); + + const dn = displayNames[slug]; + const name = (dn && (dn.short_name || dn.long_name)) || titleCaseSlug(slug); + entries.push({ id, name }); + } + + entries.sort((a, b) => a.id.localeCompare(b.id)); + + const output = { + $comment: + "Generated by build/generate-service-manifest.mjs from localstack-docs " + + "coverage data. Regenerate on demand only (when noticed out of date); " + + "do not edit by hand. Availability (community/pro) is intentionally omitted.", + services: entries, + }; + + const outPath = path.resolve( + import.meta.dirname, + "..", + "resources", + "service-manifest.json", + ); + fs.writeFileSync(outPath, `${JSON.stringify(output, null, "\t")}\n`); + console.log( + `Wrote ${entries.length} services to ${path.relative(process.cwd(), outPath)}`, + ); +} + +main(); diff --git a/eslint.config.mjs b/eslint.config.mjs index a48779d..9b2c21d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -7,6 +7,11 @@ import tseslint from "typescript-eslint"; export default defineConfig( includeIgnoreFile(fileURLToPath(new URL(".gitignore", import.meta.url))), + { + // Built webview assets are ignored via a nested .gitignore that + // includeIgnoreFile() above does not pick up. + ignores: ["resources/app-inspector/dist/"], + }, { rules: { "object-shorthand": ["error", "always"], diff --git a/openspec/changes/archive/2026-06-23-integrate-resource-browser/.openspec.yaml b/openspec/changes/archive/2026-06-23-integrate-resource-browser/.openspec.yaml new file mode 100644 index 0000000..6c351ac --- /dev/null +++ b/openspec/changes/archive/2026-06-23-integrate-resource-browser/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-21 diff --git a/openspec/changes/archive/2026-06-23-integrate-resource-browser/design.md b/openspec/changes/archive/2026-06-23-integrate-resource-browser/design.md new file mode 100644 index 0000000..d30fb2a --- /dev/null +++ b/openspec/changes/archive/2026-06-23-integrate-resource-browser/design.md @@ -0,0 +1,159 @@ +## Context + +The LocalStack Toolkit is a VS Code extension built with a small **plugin system** (`src/plugins.ts`): `extension.ts` constructs shared dependencies (status trackers, telemetry, output channel) and passes them as `PluginOptions` to each plugin's factory. Originally the only tree view, `localstack.instances`, was created inside the `app-inspector-webview` plugin and showed a single instance node with `Status` and `App Inspector` children. The build uses **esbuild** (extension, CJS, `external: ["vscode"]`) plus **Vite** (the App Inspector React webview). Tests run on **mocha + @vscode/test-cli**. Conventions: kebab-case filenames, **tab** indentation, `node:` import protocol, explicit `.ts` extensions, `import type`, object shorthand. + +This change adds a full resource-browsing experience to that foundation: a combined `Explore` view, a `Resources` view, and a `Resource Details` view, backed by a `Focus` zod model, per-service `ServiceProvider`s, thin SDK client wrappers, and models for `ARN`, `awsConfig`, `cfnStackModel`, `regionModel`. AWS is structured as one *platform* (under `src/platforms/aws/`) so future non-AWS emulators can be added alongside it. On top of that foundation the change extends the browser to LocalStack's full published service set, uses target-aware (non-AWS-derived) iconography, and addresses a set of usability issues found during in-editor testing. + +## Goals / Non-Goals + +**Goals:** +- Three tree views in the LocalStack activity-bar container: **Explore** (combined), **Resources**, **Resource Details**, built as new code under the platform layout below. +- A combined Explore tree (Instances / Cloud Profiles / Workspace IaC) whose leaf "focus selectors" drive the Resources view; `All Resources` under Instances comes from the emulator metamodel, Cloud Profiles use wildcard focuses, and `Stack:` selectors use the CFN stack model. +- User-defined views (service/resource-type pairs) and dynamic per-profile regions and shown-profiles, persisted in workspace settings. +- All AWS code under `src/platforms/aws/` to allow future emulators; a platform-neutral `Focus` model under `src/models/`. +- Coverage of LocalStack's **full published service set**, every service a real curated provider, authored declaratively to be tractable, with no runtime discovery and no generic fallback. +- Service-and-resource-type rows carry a **target-aware** icon (LocalStack vs AWS) that bundles no AWS intellectual property. + +**Non-Goals:** +- Workspace IaC functionality (placeholder `Coming soon` only) and non-AWS platforms (only the directory structure anticipates them). +- Parent/child **nesting** of resources or cross-resource **hyperlinks** — the tree stays flat; hyperlinks are deferred. +- Interactive Resource Details behaviors (ARN/log links, "open in editor"). +- Multi-account support beyond the current single-account assumption. +- Restoring distinct per-service iconography (removed deliberately; could only return later as LocalStack's own original work). +- Landing all 117 providers in this change — it delivers the engine, the existing imperative providers, and Batch 1; later batches finish coverage. + +## Decisions + +### D1: One Explore tree provider with three sections + +A single `LocalStackViewProvider` (`src/views/localstack/`) renders all three top-level sections; the Cloud Profiles section is where AWS profiles, regions, views, and CloudFormation stacks live. The existing instance affordances are preserved by reusing the `localStackStatusTracker` and `localstack.openAppInspector` command already passed through `PluginOptions`. The view is contributed as `localstack.instances` but **named "Explore"** to avoid duplicating the "LocalStack" container title; the three view contributions carry relative `size` weights (Resources `2`, the other two `1`) for ~50/25/25 on first layout. + +### D2: The `Focus` model and the Resources / Resource Details views + +The `Focus`/`ProfileFocus`/`RegionFocus`/`ServiceFocus`/`ResourceTypeFocus` zod types and the wildcard/`default` expansion logic implement the `focus-model` and `resource-browser` specs. The `Resources` and `Resource Details` views consume a `Focus` and render it: Resources expands the focus into the tree, Resource Details describes the selected resource. + +### D3: Platform layout — `src/platforms/aws/` + +``` +src/platforms/aws/ + clients/ (account, cloudformation, dynamodb, iam, lambda, sfn, sns, sqs, sts, + Batch 1 clients) + services/ (providerFactory, serviceProvider, declarative engine + definitions, service-manifest loader) + models/ (arnModel, awsConfig, cfnStackModel, regionModel, metamodelFocus) +src/views/ + localstack/ (combined Explore view: provider + tree items + focus-selector logic + settings/commands) + resources/ (Resources view) + resource-details/ (Resource Details webview) +src/models/focus.ts (platform-neutral Focus model) +``` +`ProviderFactory` abstracts service lookup; a future emulator adds `src/platforms//`. + +### D4: AWS SDK clients and LocalStack share one provider stack via `endpoint_url` + +The `localstack` profile (written by the existing configure-aws plugin) sets `endpoint_url` to the emulator, so the *same* `ServiceProvider`s list/describe against LocalStack and against real AWS. Every client constructor goes through `AWSConfig.getClientConfig`, so the endpoint is always honored. (An early bug where six clients constructed `{ profile, region }` directly — causing "security token invalid" against real AWS — was fixed by routing all of them through `getClientConfig`.) + +### D5: Single source of truth for the LocalStack endpoint + +The instance node label, the metamodel fetch, and the LocalStack-profile SDK calls all use the `localstack` profile's `endpoint_url` from `~/.aws/config` (via a `getLocalStackEndpoint()` helper that falls back to the Toolkit's DNS-based default), rather than a hardcoded `localhost.localstack.cloud:4566`, which is wrong on DNS-rebind-protected machines. + +### D6: LocalStack `All Resources` = metamodel-selected services/types + wildcard ARNs + +The metamodel (`/_localstack/pods/state/metamodel`) is shaped `account → Service (PascalCase) → region → apiOperation → rawResponse` (see the captured `metamodel-sample.json`) — a transpose of the Focus shape. `metamodelFocus.ts` fetches it (lenient parse — the payload can contain raw control characters), filters to account `000000000000`, maps each PascalCase label to a manifest service id (`toLowerCase()` plus an override table, e.g. Step Functions → `states`), dedups the `""` global-region mirror, and emits a Focus naming the present services. **Resource ARNs are left as wildcards** so the SDK providers list live resources on drill-down; Resource Details always uses the SDK `describeResource` path, never the metamodel's captured fields. A present service with no registered provider is omitted and logged. + +The focus also names only the **present resource types**, not every registered type of a present service: each resource type maps to the metamodel list operation that signals its presence (e.g. SSM `describeParameters` → Parameters), and only types whose operation key appears are included. If an operation maps to no known type (or a type declares no op), the service's full type set is included and the gap is logged, so a mapping gap never hides resources. + +### D7: Settings persistence + +Workspace-scoped configuration keys (falling back to Global when no folder is open, via a shared `configTarget()` so writes never fail with "no workspace is opened"): +- `localstack.cloudProfiles.regions`: `profileName → string[]` of added regions. +- `localstack.cloudProfiles.filters`: `profileName → [{ name, resources: { service, resourceType }[], scope }]`, where `scope` is `{ region }` or `{ allRegions: true }`. +- `localstack.cloudProfiles.shown`: `string[]` of shown profiles (opt-in; unset ⇒ `["default"]`, or the first profile when none is named `default`; an empty set is honored). +- `localstack.instanceViews`: instance views, stored under a **dedicated** key (not the `cloudProfiles.filters` map) to avoid colliding with the bundled `localstack` cloud profile's region views; the `SavedFilter` type is reused with a placeholder scope that instance rendering ignores. + +The view refreshes on `onDidChangeConfiguration` for these keys. + +### D8: Single-select focus selectors + +The view is **single-select**: activating one focus selector deactivates any other, and `computeFocus` returns the single selected selector's focus (`undefined` when nothing focusable is selected). The tree view is registered once with `canSelectMany: false`. (An earlier multi-select design — a `localstack.focus.multiSelect` setting toggled from the view title menu, re-registering the tree view to flip `canSelectMany`, with focuses merged via `mergeFocuses` — proved unreliable and was removed entirely, including the setting, the enable/disable commands, the `localstack.multiSelect` context key, and `mergeFocuses`. It may be rethought and reintroduced later.) + +### D9: Add/Edit/Remove views and regions via native VS Code primitives + +VS Code cannot show a custom multi-field modal without a webview, so these flows compose `showInputBox`/`showQuickPick`/`createQuickPick`. **Views** use a wizard: name (validated unique within the profile/instance, and `All Resources` reserved case-insensitively), then a multi-select of **service / resource-type pairs** (label `""`), then — for Cloud Profiles regions only — a region scope (`This region only` / `All regions in this profile`). The instance entry point omits the scope step. Views are editable (re-run the wizard pre-populated) and removable (modal confirmation). **Regions** are managed by a single `Select Regions...` multi-select that replaces the user-added set in one action; the configured default region is always shown and never removable. **Shown profiles** are managed by `Select Profiles...`. + +### D10: Row actions are inline icons *and* context-menu items + +A tree row's `...` overflow only renders alongside an inline action, so each per-row action is contributed both as an inline `view/item/context` entry (with an icon) and as a non-inline entry (right-click menu). Final inline icons: a pencil for `Select Profiles...` / `Select Regions...` / `Edit View...`, a plus for `Add View...`, and a trash for `Remove Region` / `Remove View`. (This reverses an interim "everything in the `...` overflow" attempt that left rows with no visible toolbar.) Focus-selector rows carry a transparent `blank` icon so the instance's `View: All Resources` aligns under `App Inspector`; this is the only use of `blank` — other iconless rows render with no icon column. + +### D11: Merged service + resource-type row; Resource Details as a webview table + +The Resources tree fuses the service and resource-type levels into one `ResourceServiceTypeTreeItem` (`label` = service name, dimmed `description` = plural type), so a multi-type service renders one row per type and ARN leaves carry no icon. Resource Details is a `WebviewViewProvider` rendering a self-contained, CSP-locked, theme-variable-styled key/value table, formatting values per `FieldType` (`JSON` pretty-printed, `LONG_TEXT` wrapped, `ARN`/`LOG_GROUP` monospace, others plain), with HTML-escaped values; interactions (links, open-in-editor) are deferred. The field column is capped at `table-layout: fixed; width: 33%` with `overflow-wrap: break-word`, so a long label wraps instead of consuming the row. + +### D12: Target-aware row icon, not a service icon + +The row icon denotes the profile **target**, not the service: `ThemeIcon("localstack-logo")` for a LocalStack-targeted profile, the built-in `ThemeIcon("cloud")` for AWS. `isLocalStack` is resolved once per profile in `makeResourceProfiles` — true when the profile resolves to a custom/local `endpoint_url` or is the synthetic `localstack` instance profile — stored on `ResourceProfileTreeItem`, and read by the row via its parent chain. `ServiceProvider.getIconPath` and the 6 AWS-derived SVGs are deleted; `ServiceProvider` returns to being purely about resource data. Trade-off: a profile pointed at a non-LocalStack custom endpoint would also get the LocalStack mark — accepted as a rare edge case, refinable later by host-pattern matching. + +### D13: Static service manifest as the single source of truth + +`resources/service-manifest.json` (bundled) + a loader under `src/platforms/aws/services/`. A checked-in generator reads `localstack-docs/src/data/coverage/*.json` and emits one `{ id, name }` per service, where `id` is **AWS's own service code** (e.g. `s3`, `secretsmanager`, `cognito-idp`, `logs`, `events`, `states`). Every service is included regardless of community/pro availability, and availability is neither stored nor displayed (the browser also targets real AWS, so all services are treated as fully available). Regeneration is **on-demand only**. The supported set is never discovered from a running service (`/_localstack/health`, Cloud Control, scraping) — the published list is the contract, and cloud profiles have no such endpoint anyway. + +### D14: Every service is curated — no generic tier — via a declarative engine + +`ProviderFactory` registers a curated provider per manifest service; there is no generic/Cloud-Control fallback (a service with no provider is simply absent during rollout). A **completeness test** asserts every manifest id has a provider — green is the definition of done. To make ~117 services tractable, a service is authored as data: + +``` +defineService("s3", "S3", { + bucket: { + singular: "Bucket", plural: "Buckets", + list: c => c.listBuckets().Buckets, id: b => b.Name, + cfn: "AWS::S3::Bucket", detailFrom: "self", + detail: [ { label: "Name", path: "Name", type: NAME }, ... ], + metamodelOp: "listBuckets", + }, +}) +``` + +A shared engine adapts a definition to the `ServiceProvider` interface (resource types, `getResourceArns`, `describeResource` by walking each field's `path`, CFN mapping, and the op→type map for D6). The imperative `ServiceProvider` class stays as the **escape hatch** for services that can't be expressed declaratively. Resource types are an editorial curation layer (coverage data has operations, not resource types); CFN type names are taken from existing knowledge, committed once, not verified against live CFN. Listing/detail use per-service SDK `List*`/`Describe*` calls through `getClientConfig`; identifiers are the ARN where available, else the primary id (the leaf tolerates a non-ARN identifier). + +### D15: Detail fields are generated by importance, then refined + +Detail-field selection is a build-time authoring aid: a dev-time generator reads each resource type's Describe/Get (or list-item) shape from **offline** AWS API models (`aws-sdk` v2 `apis/*.normal.json` or botocore `service-2.json` — generator-only, never bundled), ranks members by importance (identifiers/names → status → type → timestamps → scalars; nested collapsed/dropped; metadata/pagination/blobs excluded; capped ~12), maps each to a `FieldType`, and emits the `detail` data spec into the committed definition. At runtime the view renders that committed, typed subset — not the raw response. Multi-call or list-only services get a partial first cut and are hand-finished. + +### D16: CloudFormation stack listing maps curated resources + +`cfnStackModel` queries a stack's resources and groups them by service/resource type. A resource is shown when its service maps to a registered curated provider; it is skipped (and logged) only when unrepresentable — its service is not yet curated, or a required identifying field is absent — rather than aborting the whole stack. + +### D17: Region fallback in `getClientConfig` + +When `getClientConfig(profile, region)` receives an empty/undefined region, it falls back to `getRegionForProfile(profile)` then `us-east-1`. This single choke point fixes describe for region-less ARNs (S3 `arn:aws:s3:::name`, IAM) without threading a focus region through every provider signature; listing is unaffected because it always passes the focus region. + +### D18: Live filter resolution + command-driven refresh + +Saved-view focus producers resolve their definition live by (profile, name) from settings at selection/refresh time, rather than capturing a `SavedFilter` snapshot at tree-build time. The add/edit/remove command handlers refresh the Resources view after mutating settings (the `ResourceViewProvider` is threaded into `registerLocalStackCommands`). So editing the active view reflects immediately; removing it makes the live lookup yield nothing and the Resources view reverts to its placeholder. + +### D19: Instance views intersect the live metamodel + +An instance view's focus is computed by intersecting the live metamodel: `computeMetamodelFocus(endpoint)` kept to only the chosen `{ service, resourceType }` pairs. This keeps instance views consistent with the instance's `View: All Resources` and never lists a type with nothing deployed. (Correctness depends on D6 naming only present resource types.) + +### D20: Build, dependencies, testing + +esbuild bundles the AWS SDK with `external: ["vscode"]`; only used clients are imported. Dependencies: `@aws-sdk/client-{account,cloudformation,dynamodb,iam,lambda,sfn,sns,sqs,sts}` + the Batch 1 service clients (pinned `3.901.0`) + `js-ini`; `zod` already present. The mocha test suite covers: metamodel→Focus translation (fixture-based), single-select `computeFocus`, region fallback, live filter resolution, instance-view metamodel intersection, manifest loading/label-mapping, declarative-engine execution, and per-service Batch 1 list/describe/CFN mapping. A skipped "done-gate" completeness test tracks remaining service batches. + +## Risks / Trade-offs + +- **Metamodel payload shape / label set** → a real payload is captured and decoded; the label→id map lives in one place and is fixture-tested. Raw control characters in the JSON are handled by a lenient parse. +- **Multi-account metamodels** → v1 filters to account `000000000000` (consistent with one-account-per-profile Cloud Profiles); revisit if multi-account emulator state becomes a real need. +- **Step Functions metamodel label** → Step Functions is not currently returned by the metamodel (a known emulator bug); a best-guess `→ states` override is carried but treated as unverified. +- **Misclassifying a non-LocalStack custom endpoint as LocalStack** → accepted edge case; refinable by host-pattern matching. +- **Scale & maintenance of 117 services × fields** → declarative definitions + the D15 generator minimize hand-work; batched delivery keeps each increment reviewable; the completeness test prevents silent gaps; a test that curated ids match manifest ids guards drift. +- **Auto-selected fields may mis-rank** → accepted; fields are committed and editable. Declarative-engine expressiveness gaps are covered by the imperative escape hatch; parity of the migrated providers is diff-tested before removing imperative versions. +- **Wrong default region for a genuinely regional region-less ARN** → only affects effectively-global resources (S3, IAM); the profile default is sensible and far better than failing. +- **Over-refreshing the Resources view on any view mutation** → cheap re-list; non-active views recompute to the same focus. Renaming the active view orphans it (reverts to placeholder) — accepted. + +## Migration Plan + +Additive on an unreleased branch; no runtime data migration. Order: build the Focus model and AWS platform code → build the combined Explore view, metamodel focus, settings, and view wiring → UX refinements from in-editor feedback → declarative engine + manifest + completeness test + Batch 1 → target-aware icons → browser fixes (region fallback, details column, remove multi-select, live filter resolution, metamodel resource-type accuracy, instance views). Rollback is reverting the branch. An earlier iteration shipped an opt-in multi-select that proved unreliable and was removed before release (D8); the `localstack.focus.multiSelect` setting is ignored if present. Existing `Status`/`App Inspector` affordances and the `localstack.cli.location` setting are untouched. Subsequent service batches and the parity migration of the imperative providers continue until the completeness test is green. + +## Open Questions + +Resolved during the work: Vision 1 (metamodel selects, SDK lists) over a generic metamodel data-source provider; Resource Details always via the SDK; single account for v1; show the bundled `localstack` profile in Cloud Profiles; per-profile/per-region view scope with an "all regions" option; no live-CFN verification; on-demand manifest regeneration; availability neither stored nor shown; detail fields generated-then-refined; not-yet-curated services are simply absent. + +Carried for later: refine the LocalStack signal to host-pattern matching; confirm the Step Functions metamodel label once the emulator emits it; whether the metamodel ever records an operation with an empty result (would need a "response non-empty" presence test instead of operation-key-present); whether any resource type is listed via multiple operations (the sample is 1:1). diff --git a/openspec/changes/archive/2026-06-23-integrate-resource-browser/metamodel-sample.json b/openspec/changes/archive/2026-06-23-integrate-resource-browser/metamodel-sample.json new file mode 100644 index 0000000..4a4504a --- /dev/null +++ b/openspec/changes/archive/2026-06-23-integrate-resource-browser/metamodel-sample.json @@ -0,0 +1,304 @@ +{ + "000000000000": { + "EventBridge": { + "us-east-1": { + "listEventBuses": { + "EventBuses": [ + { + "Name": "default", + "Arn": "arn:aws:events:us-east-1:000000000000:event-bus/default", + "CreationTime": "2026-06-21T18:04:47.863Z", + "LastModifiedTime": "2026-06-21T18:04:47.863Z" + } + ] + } + } + }, + "S3": { + "us-east-1": { + "listBuckets": { + "Buckets": [ + { + "Name": "cdk-hnb659fds-assets-000000000000-us-east-1", + "CreationDate": "2026-06-21T18:04:48.000Z", + "BucketRegion": "us-east-1", + "BucketArn": "arn:aws:s3:::cdk-hnb659fds-assets-000000000000-us-east-1" + } + ], + "Owner": { + "ID": "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a" + } + } + } + }, + "IAM": { + "us-east-1": { + "listRoles": { + "Roles": [ + { + "Path": "/", + "RoleName": "cdk-hnb659fds-cfn-exec-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAD6AABTQKO", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-cfn-exec-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.038Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { "Service": "cloudformation.amazonaws.com" } + } + ], + "Version": "2012-10-17" + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-deploy-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAIBIRR5HWV", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-deploy-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.666Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-file-publishing-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAHZZBLFSX4", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-file-publishing-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.674Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-image-publishing-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAA5GBH3MK4", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-image-publishing-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.678Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-lookup-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAJF4WBPPSG", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-lookup-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.683Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + } + ], + "IsTruncated": false + } + }, + "": { + "listRoles": { + "Roles": [ + { + "Path": "/", + "RoleName": "cdk-hnb659fds-cfn-exec-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAD6AABTQKO", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-cfn-exec-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.038Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { "Service": "cloudformation.amazonaws.com" } + } + ], + "Version": "2012-10-17" + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-deploy-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAIBIRR5HWV", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-deploy-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.666Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-file-publishing-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAHZZBLFSX4", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-file-publishing-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.674Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-image-publishing-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAA5GBH3MK4", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-image-publishing-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.678Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-lookup-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAJF4WBPPSG", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-lookup-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.683Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + } + ], + "IsTruncated": false + } + } + }, + "SSM": { + "us-east-1": { + "describeParameters": { + "Parameters": [ + { + "Name": "/cdk-bootstrap/hnb659fds/version", + "Type": "String", + "LastModifiedDate": "2026-06-21T18:04:47.546Z", + "LastModifiedUser": "N/A", + "Version": 1, + "Tier": "Standard", + "DataType": "text" + } + ] + } + } + }, + "CloudFormation": { + "us-east-1": { + "listStacks": { + "StackSummaries": [ + { + "StackId": "arn:aws:cloudformation:us-east-1:000000000000:stack/CDKToolkit/a3f70502-8248-49ea-b7e1-df8f6dea5a35", + "StackName": "CDKToolkit", + "CreationTime": "2026-06-21T18:04:47.226Z", + "LastUpdatedTime": "2026-06-21T18:04:47.226Z", + "StackStatus": "CREATE_COMPLETE", + "DriftInformation": { "StackDriftStatus": "NOT_CHECKED" } + } + ] + } + } + } + } +} diff --git a/openspec/changes/archive/2026-06-23-integrate-resource-browser/proposal.md b/openspec/changes/archive/2026-06-23-integrate-resource-browser/proposal.md new file mode 100644 index 0000000..035bd6b --- /dev/null +++ b/openspec/changes/archive/2026-06-23-integrate-resource-browser/proposal.md @@ -0,0 +1,52 @@ +## Why + +The LocalStack Toolkit originally showed only a minimal "LocalStack" tree (instance status + an App Inspector launcher). This change builds a full resource-browsing experience into the Toolkit — a combined Explore tree, a Resources tree, and a Resource Details panel backed by AWS SDK service providers — so users get a single sidebar where they can browse the resources running in their local emulator *and* in their real AWS cloud profiles. + +This is also the moment to generalize: the Toolkit is structured so that AWS is one *platform* among future (non-AWS) emulators, rather than hard-coding AWS throughout — and to make the browser reflect the **full published set of services LocalStack supports**, not just a hand-picked few. + +## What Changes + +### Combined Explore view and resource browser + +- **Add three tree views** to the LocalStack activity-bar container: **Explore**, **Resources**, and **Resource Details**. The view is named "Explore" (not "LocalStack") to avoid duplicating the container title; first-open sizing is ~50% Resources, ~25% each for Explore and Resource Details. +- **Replace** the minimal LocalStack instances tree with the combined **Explore** tree of three sections: + - **LocalStack Instances** → an instance node labeled `AWS (): ` (endpoint derived from the `localstack` profile's `endpoint_url`, status folded onto the line). When running it exposes `App Inspector`, a `View: All Resources` focus selector, and any saved instance views. + - **Cloud Profiles** → one node per AWS profile from `~/.aws/config` (default-only until the user selects more) → the profile's default region plus user-added regions → `View: All Resources`, saved views, and one `Stack: ` selector per CloudFormation stack. + - **Workspace IaC** → a `Coming soon` placeholder. +- **Focus selectors** (`View: All Resources`, `View: `, `Stack: `) drive the active Focus for the Resources view. Selection is **single-select**. +- **User-defined views** (`Add View...` / `Edit View...` / `Remove View`) scope a focus to a set of **service / resource-type pairs**, persisted in workspace settings; available under Cloud Profiles regions and under the LocalStack Instance node. **Dynamic regions** (`Select Regions...`) and **shown profiles** (`Select Profiles...`) are likewise persisted. Per-row actions appear as inline icons and in the right-click context menu. +- **Resource Details** renders as a self-contained, themed **webview table** of the selected resource's fields, fetched via the AWS SDK `describeResource` path (same code path for LocalStack and real AWS). +- **Relocate all AWS-specific code** under `src/platforms/aws/` (clients, services, models) so future emulators can add `src/platforms//`; the `Focus` model stays platform-neutral under `src/models/`. + +### Full LocalStack service coverage (manifest + declarative providers) + +- **Add a static service manifest** generated from LocalStack's published coverage data (`localstack-docs/src/data/coverage/*.json`, 117 services), keyed by AWS service code. It is the single source of truth for which services exist; the system SHALL NOT query a running emulator or any discovery API to decide what is supported. +- **Every supported service is a curated provider** — declaring its resource types, listing live resources, and describing a resource with a per-service field set. There is no generic/fallback provider tier and no Cloud Control listing. +- **Introduce a declarative provider format + engine** so curating ~117 services is tractable; the imperative `ServiceProvider` class remains as an escape hatch. **Completeness** (every manifest service has a provider) is the definition of done, enforced by a test, and delivered in **batches**. +- The metamodel "All Resources" view and CloudFormation stack listing both map emulator service labels to manifest ids and render every present service that has a curated provider. + +### Target-aware icons (no AWS-derived art) + +- **Remove all AWS-derived assets** (the hand-edited service SVGs) and the per-service `getIconPath` API. +- **Add a target-aware row icon**: the combined service-and-resource-type row shows `ThemeIcon("localstack-logo")` when its profile targets a LocalStack emulator and the built-in `ThemeIcon("cloud")` when it targets real AWS — both themed, neither AWS-derived. The target is resolved per profile from its `endpoint_url`. + +## Capabilities + +### New Capabilities +- `focus-model`: the Focus data structure is a fundamental part of this application, representing the mapping of (profiles → regions → services → resource types → ARNs). This data structure stores the set of resources that will be displayed in the resource view. There are many ways to create a `Focus` data structure, including direct querying of LocalStack's state, or examining the content of an existing CloudFormation stack. +- `localstack-explorer-view`: the combined "Explore" tree — its three sections, focus selectors, single-select behavior, user-added regions/views/shown-profiles persisted to workspace settings, and the inline/context row actions. +- `localstack-metamodel`: translating the emulator's pods-state metamodel endpoint into a Focus for the LocalStack Instances section (present services *and* present resource types only). +- `resource-browser`: the Resources and Resource Details views driven by the active focus, the target-aware row icon, and the AWS platform service providers that list and describe resources via the AWS SDK against a profile's endpoint. +- `service-catalog`: the static, coverage-derived service manifest; the requirement that every manifest service has a curated provider (no generic tier, no runtime discovery); and the declarative provider format/engine used to author them. + +### Modified Capabilities + + +## Impact + +- **New views/commands/settings** in `package.json` (`contributes.views`, `commands`, `configuration`, `menus`, `icons`). +- **Replaces** `src/plugins/app-inspector-webview.ts`'s `InstancesTreeDataProvider` with the new Explore view; the App Inspector webview launcher is preserved. +- **New code** under `src/platforms/aws/` (clients, services, models, the service manifest + loader, the declarative engine and per-service definitions) and `src/views/` (localstack/resources/resource-details), following the Toolkit's conventions (kebab-case files, tab indentation, `node:`/`.ts` imports). +- **New dependencies**: `@aws-sdk/client-*` packages (the original nine plus the Batch 1 services), `js-ini`; bundled by the existing esbuild config. No new runtime dependencies for the icon or fix work. +- **Settings**: `localstack.cloudProfiles.{regions,filters,shown}` and `localstack.instanceViews`. The abandoned `localstack.focus.multiSelect` setting is removed; any stored value is ignored (behavior is single-select). +- **Tests**: model/view tests; metamodel→Focus translation, filter/region persistence, single-select `computeFocus`, region fallback, live filter resolution, instance-view metamodel intersection, manifest loading/label-mapping, declarative-engine execution, and per-service Batch 1 list/describe/CFN mapping. A completeness "done-gate" test tracks remaining service batches. diff --git a/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/focus-model/spec.md b/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/focus-model/spec.md new file mode 100644 index 0000000..d81a74f --- /dev/null +++ b/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/focus-model/spec.md @@ -0,0 +1,34 @@ +## ADDED Requirements + +### Requirement: Focus data structure + +The system SHALL represent a "focus" as a hierarchical structure of profiles → regions → services → resource types → ARNs. A focus SHALL carry a `version` string and a list of profiles; each profile SHALL have an `id` and a list of regions; each region SHALL have an `id` and a list of services; each service SHALL have an `id` and a list of resource types; each resource type SHALL have an `id` and a list of ARN strings. The structure SHALL be validated against a schema before use. + +#### Scenario: Valid focus is accepted + +- **WHEN** a focus object matching the schema (version, profiles, regions, services, resource types, arns) is parsed +- **THEN** parsing succeeds and the typed focus object is returned + +#### Scenario: Malformed focus is rejected + +- **WHEN** a focus object missing required fields or with mistyped fields is parsed +- **THEN** parsing fails with a descriptive validation error and no focus is returned + +### Requirement: Wildcard and default selectors + +The system SHALL support wildcard (`*`) selectors for profiles, regions, services, resource types, and ARNs, and a `default` selector for regions. A wildcard at a level SHALL mean "expand dynamically to all available items at that level when rendered"; a `default` region SHALL mean "the profile's configured default region". Non-wildcard ids SHALL be used literally. + +#### Scenario: Wildcard service expands to all supported services + +- **WHEN** a region's services list contains exactly one service with id `*` +- **THEN** consumers SHALL expand it to every supported service provider, each with wildcard resource types and ARNs + +#### Scenario: Default region resolves to the profile's configured region + +- **WHEN** a profile's regions list contains exactly one region with id `default` +- **THEN** consumers SHALL resolve it to the region configured for that profile, or surface an error if none is configured + +#### Scenario: Wildcard ARN triggers live listing + +- **WHEN** a resource type's arns list is exactly `["*"]` +- **THEN** consumers SHALL list the actual ARNs from the platform rather than using literal values diff --git a/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/localstack-explorer-view/spec.md b/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/localstack-explorer-view/spec.md new file mode 100644 index 0000000..df0663f --- /dev/null +++ b/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/localstack-explorer-view/spec.md @@ -0,0 +1,293 @@ +## ADDED Requirements + +### Requirement: Combined Explore tree view + +The system SHALL provide a single tree view named "Explore" in the LocalStack activity-bar container with three top-level sections in this order: **LocalStack Instances**, **Cloud Profiles**, and **Workspace IaC**. The view name SHALL be "Explore" (not "LocalStack") to avoid visual duplication with the "LocalStack" activity-bar container title. No separator nodes SHALL be rendered between the sections. + +#### Scenario: Three sections are shown at the root + +- **WHEN** the Explore view is rendered +- **THEN** the three top-level section nodes appear in order — "LocalStack Instances", "Cloud Profiles", and "Workspace IaC" — with no separator node between them + +#### Scenario: View name avoids container duplication + +- **WHEN** the Explore view is rendered inside the "LocalStack" activity-bar container +- **THEN** the view's name reads "Explore", not "LocalStack" + +### Requirement: Initial view sizing + +When the LocalStack activity-bar container is first opened, the system SHALL allocate approximately 50% of the available vertical space to the **Resources** view and approximately 25% each to the **Explore** and **Resource Details** views, via relative `size` weights on the view contributions (Resources `2`, the other two `1`). This applies to first layout; the user's subsequent manual resizing persists and overrides these defaults. + +#### Scenario: Resources view gets half the height on first open + +- **WHEN** the LocalStack activity-bar container is opened for the first time +- **THEN** the Resources view occupies roughly half the vertical space and the Explore and Resource Details views occupy roughly a quarter each + +### Requirement: LocalStack Instances section + +Under **LocalStack Instances**, the system SHALL show an instance node labeled `AWS (): `, where the endpoint is derived from the endpoint the Toolkit is configured to use (the `localstack` profile's `endpoint_url` in `~/.aws/config`) rather than a hardcoded value, and `` is the live emulator status with its first letter capitalized (e.g. `Running`, `Stopped`). The status SHALL be presented on the same line as the endpoint, NOT as a separate `Status` child node. The instance node SHALL show child nodes only when the emulator is running: an `App Inspector` node that opens the App Inspector when clicked, a `View: All Resources` focus selector, and one `View: ` focus selector per user-defined view saved for the instance. When the emulator is not running, the instance node SHALL have no children and SHALL render as a non-expandable line (no twistie). + +The instance node SHALL offer an `Add View...` action (matching the Cloud Profiles region affordance); each instance `View: ` row SHALL offer `Edit View...` / `Remove View` actions. Instance views SHALL be persisted in a dedicated saved-view store (separate from the Cloud Profiles per-region views, to avoid colliding with the bundled `localstack` cloud profile) and SHALL NOT carry a region scope (the region-scope step is omitted for instance views). Selecting an instance view SHALL produce a focus that **intersects the live metamodel**: starting from the resources actually deployed in the running instance (the same source as `View: All Resources`) and keeping only the chosen service/resource-type pairs, so an instance view never lists a type with nothing deployed. + +#### Scenario: Stopped instance shows the status line and no children + +- **WHEN** the emulator is stopped and the endpoint is `localhost:4566` +- **THEN** the instance node is labeled `AWS (Stopped): localhost:4566`, with no separate `Status` child node, no `App Inspector`/`View: All Resources` children, and no expand twistie + +#### Scenario: Running instance exposes app inspector and a focus selector + +- **WHEN** the emulator is running and the LocalStack Instances section is expanded +- **THEN** the instance node shows an `App Inspector` child and a `View: All Resources` focus selector child (and no standalone `Status` child) + +#### Scenario: Children appear and disappear with live status + +- **WHEN** the emulator transitions from stopped to running (or vice versa) +- **THEN** the instance node gains (or loses) its `App Inspector` and `View: All Resources` children without a manual refresh + +#### Scenario: Instance label reflects the configured endpoint + +- **WHEN** the `localstack` profile's `endpoint_url` is `http://127.0.0.1:4566` (e.g. DNS rebind protection is active) +- **THEN** the instance node's `host:port` is `127.0.0.1:4566`, matching the endpoint the SDK and metamodel calls use, not a hardcoded `localhost.localstack.cloud:4566` + +#### Scenario: Status reflects the running emulator + +- **WHEN** the emulator status changes (e.g. stopped → running) +- **THEN** the instance node's label updates to the new capitalized status (e.g. `AWS (Running): …`) without requiring a manual refresh, and the `App Inspector` node's description updates accordingly (see App Inspector scenarios) + +#### Scenario: Status detects containers started under alternate names + +- **WHEN** the emulator container is started by the `lstk` CLI under the name `localstack-aws` (rather than `localstack-main`) +- **THEN** the status reflects `Running` once the emulator is healthy, because the status tracker watches all known emulator container names — `localstack-main` and `localstack-aws` — for both the initial `docker inspect`/`ps` check and the live `docker events` stream + +#### Scenario: Adding a view to the instance + +- **WHEN** the emulator is running and the user invokes `Add View...` on the instance node, names the view, and selects one or more service/resource-type pairs +- **THEN** a `View: ` focus selector appears under the instance node and is saved for the instance (no region-scope step is shown) + +#### Scenario: An instance view shows only deployed resources of the chosen types + +- **WHEN** the user selects an instance view whose chosen pairs include a resource type with nothing deployed in the running instance +- **THEN** the Resources view shows only the deployed resources among the chosen pairs, consistent with `View: All Resources`, and does not list the empty type + +### Requirement: App Inspector node + +The `App Inspector` node under an instance SHALL display a magnifying-glass icon (the `search` theme icon) and SHALL open the existing App Inspector webview when clicked. Because the node is only shown when the emulator is running, its description SHALL read `Click to open`. + +#### Scenario: App Inspector shows a magnifying-glass icon + +- **WHEN** the running instance node is expanded +- **THEN** the `App Inspector` node displays the `search` (magnifying glass) theme icon and a `Click to open` description + +#### Scenario: App Inspector opens the existing webview + +- **WHEN** the user clicks the `App Inspector` node +- **THEN** the existing App Inspector webview panel opens (behavior unchanged from before this change) + +### Requirement: Cloud Profiles section + +Under **Cloud Profiles**, the system SHALL show one node per AWS profile discovered in `~/.aws/config` (including `default` and the bundled `localstack` profile), labeled `AWS: `. Each profile SHALL show its configured default region plus any user-added regions; each region SHALL contain a `View All Resources` focus selector, the filters applicable to that region, and one `Stack: ` focus selector per active CloudFormation stack in that region. Region-level actions (`Add View...`, and `Remove Region` for user-added regions) are available on the region row as inline icons and in the right-click context menu, not as child action nodes. The profile's region set is managed from the profile row's `Select Regions...` action (an inline filter icon, also in the context menu), also not via a child action node. + +#### Scenario: Profiles are listed from AWS config + +- **WHEN** the Cloud Profiles section is expanded and `~/.aws/config` defines profiles `default` and `staging` +- **THEN** nodes `AWS: default` and `AWS: staging` appear + +#### Scenario: A region lists its focus selectors + +- **WHEN** a profile's region is expanded +- **THEN** it shows `View: All Resources`, the filters applicable to that region (each labeled `View: `), and one `Stack: ` selector per active CloudFormation stack (the `Add View...` action is on the region row, not a child node) + +#### Scenario: The bundled localstack profile is shown + +- **WHEN** `~/.aws/config` includes the bundled `localstack` profile +- **THEN** an `AWS: localstack` node appears under Cloud Profiles, in addition to the LocalStack Instances section + +#### Scenario: Invalid credentials surface an error node + +- **WHEN** a profile cannot be queried (e.g. invalid credentials) +- **THEN** an error node is shown for that profile rather than failing the whole view + +### Requirement: Select which Cloud Profiles are shown + +By default, only the profile named `default` SHALL be shown under Cloud Profiles; when no profile named `default` exists, the first discovered profile SHALL be shown instead. The set of shown profiles SHALL be persisted in `localstack.cloudProfiles.shown` (a list of profile names) and SHALL NOT modify `~/.aws/config`. When the setting is **unset**, the system SHALL apply the default-only behavior above; when it is set (including the empty list), the system SHALL honor it exactly. The Cloud Profiles section row SHALL offer a `Select Profiles...` action (an inline gear icon, also in the right-click context menu) that opens a multi-select picker of all discovered profiles, pre-checked with the currently shown set; confirming SHALL persist the checked names as the shown set. When the shown set is empty, the section SHALL render a single non-interactive `No profiles selected` placeholder. + +#### Scenario: Only the default profile is shown initially + +- **WHEN** the Cloud Profiles section is first rendered, `localstack.cloudProfiles.shown` is unset, and `~/.aws/config` defines `default` and `staging` +- **THEN** only `AWS: default` appears, and `AWS: staging` is hidden until explicitly enabled + +#### Scenario: First profile shown when none is named default + +- **WHEN** `localstack.cloudProfiles.shown` is unset and no profile is named `default` +- **THEN** the first discovered profile is shown + +#### Scenario: Enabling additional profiles + +- **WHEN** the user opens `Select Profiles...` and checks `staging` +- **THEN** `AWS: staging` appears under Cloud Profiles and the choice is saved to `localstack.cloudProfiles.shown` without changing `~/.aws/config` + +#### Scenario: Deselecting every profile shows a placeholder + +- **WHEN** the user opens `Select Profiles...` and unchecks every profile +- **THEN** the Cloud Profiles section shows a single `No profiles selected` placeholder, and the empty selection is honored (not reset to the default) + +#### Scenario: Shown profiles persist across reloads + +- **WHEN** the workspace is reloaded after changing which profiles are shown +- **THEN** the same set of profiles is shown + +### Requirement: Settings persistence target + +All view-state writes (added regions, saved filters/views, instance views, and shown profiles) SHALL persist to Workspace settings when a workspace folder is open, and SHALL fall back to Global (User) settings when no workspace folder is open. Writing SHALL NOT fail when no folder is open. + +#### Scenario: Persists per-workspace when a folder is open + +- **WHEN** a workspace folder is open and the user changes a view-state setting (e.g. hides a profile) +- **THEN** the change is written to Workspace settings and applies to that workspace + +#### Scenario: Falls back to global with no folder open + +- **WHEN** no workspace folder is open and the user changes a view-state setting +- **THEN** the change is written to Global (User) settings without error, rather than failing with "Unable to write to Workspace Settings" + +### Requirement: Workspace IaC placeholder + +Under **Workspace IaC**, the system SHALL show a single placeholder child with the literal text `Coming soon`. + +#### Scenario: Placeholder is shown + +- **WHEN** the Workspace IaC section is expanded +- **THEN** a single non-interactive `Coming soon` node is shown + +### Requirement: Row actions are shown as inline icons and in the context menu + +The per-row actions in the Explore view SHALL be shown as inline action icons on the row (a hover toolbar) AND remain available in the row's right-click context menu. (VS Code only renders a tree row's `...` overflow alongside an inline action, so a row with no inline action has no `...` button at all; the actions are therefore contributed inline so they are always visible.) The inline icons SHALL be: an edit (pencil) icon for `Select Profiles...` (Cloud Profiles section row) and `Select Regions...` (profile row), a plus icon for `Add View...` (region rows and the instance node), a trash icon for `Remove Region` (user-added region rows), and a pencil icon for `Edit View...` plus a trash icon for `Remove View` (saved-view rows). Each of these SHALL also be contributed as a non-inline `view/item/context` item so it appears in the right-click menu. + +#### Scenario: Row actions show an inline icon + +- **WHEN** the user hovers the Cloud Profiles section row, a profile row, a region row, a user-added region row, or a saved-view row +- **THEN** the corresponding inline icon(s) are shown (pencil; pencil; plus; trash; pencil and trash respectively) and clicking one invokes the action + +#### Scenario: Inline actions are also in the context menu + +- **WHEN** the user right-clicks a row that has an inline action +- **THEN** the same action(s) are also listed in the context menu + +### Requirement: Select Regions + +The system SHALL let the user choose which regions are shown under a Cloud Profile via a `Select Regions...` action on the profile row (an inline filter icon, also in the right-click context menu) — not via a separate child node. The action opens a multi-select picker of all known regions (excluding the profile's configured default region, which is always shown). The picker SHALL be pre-checked with the regions currently shown for that profile; confirming SHALL replace the set of user-added regions (adding newly-checked and removing unchecked ones in a single action). The chosen regions SHALL be persisted per profile so they reappear on reload. + +#### Scenario: Select Regions lives on the profile row + +- **WHEN** the user views a profile row (e.g. `AWS: default`) +- **THEN** a `Select Regions...` action is available as an inline filter icon and in the right-click context menu, and there is no separate `Show more regions` child node under the profile + +#### Scenario: Selecting multiple regions persists them + +- **WHEN** the user invokes `Select Regions...` for a profile and checks several regions +- **THEN** all checked regions appear under that profile and are saved to settings + +#### Scenario: Deselecting removes a region + +- **WHEN** the user invokes `Select Regions...` and unchecks a currently-shown region +- **THEN** that region is removed from the profile (the configured default region is never offered for deselection) + +#### Scenario: Added regions survive reload + +- **WHEN** the workspace is reloaded after regions were added +- **THEN** the previously added regions are still shown under that profile + +### Requirement: Remove a region + +The system SHALL let the user remove a user-added region via a `Remove Region` action on that region row (an inline trash icon, also in the right-click context menu), removing only that one region from the workspace settings. Invoking the action SHALL first present a modal confirmation dialog; the region SHALL be removed only when the user confirms. The profile's configured default region SHALL always be shown and SHALL NOT offer a remove action. + +#### Scenario: Removing a user-added region + +- **WHEN** the user invokes Remove on a user-added region node and confirms the dialog +- **THEN** that single region is removed from the profile and from workspace settings, and other regions remain + +#### Scenario: Cancelling the confirmation keeps the region + +- **WHEN** the user invokes Remove on a user-added region node and cancels the dialog +- **THEN** the region is not removed and settings are unchanged + +#### Scenario: Default region cannot be removed + +- **WHEN** the user views the profile's configured default region node +- **THEN** no remove action is offered for it + +### Requirement: Add new view + +The system SHALL let the user define a named filter via the `Add View...` action — invoked from a region row's inline plus icon (also in the right-click context menu) and from the LocalStack Instance node — by selecting a set of **service / resource-type pairs** (e.g. `SQS — Queues`, `Lambda — Functions`), not whole services. This lets a view include some resource types of a service while excluding others. In the UX a saved filter is labeled a "view" (the actions are `Add View...`, `Edit View...`, `Remove View`); the underlying concept and the `localstack.cloudProfiles.filters` settings key are unchanged (the per-filter value stores a list of `{ service, resourceType }` pairs), and the remainder of this spec refers to the concept as a "filter" for continuity with the persisted model. For a **Cloud Profiles** region, a filter is by default scoped to the single region it was created under; the dialog SHALL offer an "apply to all regions" option that instead makes the filter available under every region of that profile. For a **LocalStack Instance** view, the region-scope step SHALL be omitted (instance views always apply to the running instance) and the view is stored in the dedicated instance-view store. The filter SHALL appear as a focus selector labeled `View: `, scoped to the chosen pairs, and SHALL be persisted with its scope. The name SHALL be unique within the profile (or instance) and SHALL NOT be `All Resources` (case-insensitive), which is reserved for the built-in all-resources selector; the dialog SHALL reject a reserved or duplicate name. + +#### Scenario: Creating a region-scoped filter + +- **WHEN** the user invokes `Add View...` from a region, names it, selects one or more service/resource-type pairs, and leaves "apply to all regions" off +- **THEN** a new focus selector labeled `View: ` appears under that region only and is saved with a single-region scope + +#### Scenario: The reserved name is rejected + +- **WHEN** the user enters `All Resources` (in any letter case) as the view name +- **THEN** the dialog rejects it as a reserved view name and does not save a filter + +#### Scenario: Creating a filter applied to all regions + +- **WHEN** the user invokes `Add View...`, selects pairs, and enables "apply to all regions" +- **THEN** the filter appears under every region of that profile and is saved with an all-regions scope + +#### Scenario: Creating an instance view omits the region-scope step + +- **WHEN** the user invokes `Add View...` on the LocalStack Instance node and selects pairs +- **THEN** no region-scope choice is presented and the view is saved for the instance + +#### Scenario: Selecting a filter scopes the Resources view to its resource types + +- **WHEN** the user selects a saved filter focus selector +- **THEN** the Resources view shows only the service/resource-type pairs chosen for that filter + +### Requirement: Edit and remove filters + +The system SHALL let the user edit or remove a saved filter via `Edit View...` / `Remove View` actions on the filter row (an inline gear icon for `Edit View...` and a trash icon for `Remove View`, also in the right-click context menu), for both Cloud Profiles region views and LocalStack Instance views. The `Edit View...` icon SHALL be the same gear (settings) icon used by `Select Profiles...` / `Select Regions...`. Editing SHALL reopen the filter wizard pre-populated with the filter's current name, services, and scope, and overwrite the saved filter with the new values. Removing SHALL first present a modal confirmation dialog and SHALL delete the filter from the workspace settings only when the user confirms. After a successful add, edit, or remove, the system SHALL refresh the Resources view so that, when the affected view is the active focus, the Resources view reflects the change (or reverts to its placeholder if the active view was removed) without the user reselecting it. + +#### Scenario: Editing a filter updates it + +- **WHEN** the user invokes Edit on a filter, changes its services, and confirms +- **THEN** the filter's saved services are updated and the focus selector reflects the new scope + +#### Scenario: Editing the active filter refreshes the Resources view + +- **WHEN** the user edits the filter that is currently the active focus +- **THEN** the Resources view refreshes to reflect the new definition without the user reselecting the view + +#### Scenario: Removing a filter deletes it + +- **WHEN** the user invokes Remove on a filter and confirms the dialog +- **THEN** the filter no longer appears under any region (or under the instance) and is deleted from workspace settings + +#### Scenario: Cancelling the confirmation keeps the filter + +- **WHEN** the user invokes Remove on a filter and cancels the dialog +- **THEN** the filter is left unchanged + +### Requirement: Focus selectors drive the active focus + +The system SHALL treat `View: All Resources`, saved filters (`View: `), and `Stack: ` nodes as focus selectors. Selecting a focus selector SHALL compute its focus and set it as the active focus for the Resources view. Focus selectors SHALL carry a transparent (`blank`) icon so their labels align with icon-bearing sibling rows — specifically so the instance's `View: All Resources` selector lines up with the `App Inspector` node. This is the only place a `blank` icon is used; other iconless rows render without one. + +#### Scenario: Selecting a focus selector updates the Resources view + +- **WHEN** the user clicks a focus selector +- **THEN** the Resources view re-renders to show the resources described by that selector's focus + +### Requirement: Single-select behavior + +The system SHALL use single-select for focus selectors: activating one focus selector SHALL deactivate any other, and the Resources view SHALL reflect exactly the most recently selected focus. The system SHALL NOT provide a multi-select mode or a toggle for one. + +#### Scenario: Selecting a focus selector replaces the active focus + +- **WHEN** the user selects a focus selector and then selects a different one +- **THEN** only the most recently selected focus is active and the Resources view reflects it alone + +#### Scenario: No multi-select toggle is present + +- **WHEN** the user opens the LocalStack view's `...` title menu +- **THEN** no enable/disable multi-select action is shown diff --git a/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/localstack-metamodel/spec.md b/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/localstack-metamodel/spec.md new file mode 100644 index 0000000..9403319 --- /dev/null +++ b/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/localstack-metamodel/spec.md @@ -0,0 +1,71 @@ +## ADDED Requirements + +### Requirement: Compute a focus from the emulator metamodel + +The `All Resources` selector under **LocalStack Instances** SHALL compute its focus from the running emulator's `/_localstack/pods/state/metamodel` endpoint, reached at the endpoint the Toolkit is configured to use (the `localstack` profile's `endpoint_url`) rather than a hardcoded URL. The payload is nested as account → service label → region → API operation → raw response. The system SHALL fetch that endpoint and translate (transpose) its JSON into a Focus that names the services and regions actually present in the emulator state, scoped to the bundled LocalStack profile. + +#### Scenario: Metamodel is translated into a focus + +- **WHEN** the user selects `All Resources` under LocalStack Instances and the emulator is running +- **THEN** the system fetches `/_localstack/pods/state/metamodel` and produces a focus listing the services and regions present in the returned state, with the service and region axes transposed into Focus order (profile → region → service) + +#### Scenario: Only present services are named + +- **WHEN** the metamodel reports state for a subset of services +- **THEN** the produced focus includes only those services (not every supported service) + +### Requirement: Service-label mapping and unsupported-service filtering + +The metamodel labels services in PascalCase (e.g. `CloudFormation`, `IAM`, `S3`). The system SHALL map each label to its manifest service-code id (case-insensitive, with a documented override table for exceptions such as Step Functions). Every service present in the metamodel that resolves to a manifest service **with a registered curated provider** SHALL be included in the produced focus — this includes services beyond the original seven, as their providers are added. The metamodel SHALL be used only to determine which services are *present* in the running instance; it SHALL NOT be used to determine which services are *supported* (that comes from the static manifest, see the `service-catalog` capability). A present service that resolves to no registered provider (e.g. not yet curated during rollout) SHALL be omitted and the omission SHALL be surfaced (e.g. via logging). + +#### Scenario: PascalCase labels map to manifest ids + +- **WHEN** the metamodel contains `CloudFormation` and `IAM` +- **THEN** the produced focus contains services with ids `cloudformation` and `iam` + +#### Scenario: A present service with a curated provider appears + +- **WHEN** the metamodel contains a service that has a registered curated provider (e.g. `S3` once S3 is curated) +- **THEN** that service is included in the produced focus + +#### Scenario: A present but not-yet-curated service is omitted and logged + +- **WHEN** the metamodel contains a service that resolves to no registered provider +- **THEN** that service is omitted from the focus and the omission is logged + +### Requirement: Global-region and account handling + +The system SHALL treat an empty-string region (`""`, the global-service mirror) as a duplicate of the same service's regional entries and SHALL NOT emit a region node with an empty id, while still representing a service that appears only under `""`. The system MAY assume a single account maps to the bundled LocalStack profile. + +#### Scenario: Empty-string region is not rendered as a region node + +- **WHEN** a service appears under both `us-east-1` and `""` with identical contents +- **THEN** the produced focus contains that service once under `us-east-1` and no region node with an empty id + +### Requirement: Live resource listing under the metamodel focus + +The focus produced from the metamodel SHALL name only the resource types actually present in the metamodel for each service/region — determined from the metamodel's API-operation keys (each resource type maps to the list operation that signals its presence, e.g. SSM `describeParameters` → Parameters) — rather than expanding every present service to all of its registered resource types. The focus SHALL leave **ARN** selectors as wildcards so the AWS service providers list the live resource ARNs from the emulator on drill-down; only the resource-type axis is narrowed by the metamodel, not the ARN axis. When the metamodel reports an operation that cannot be mapped to a known resource type (or a resource type declares no operation), the system SHALL fall back to including that service's full resource-type set and SHALL log the gap, so a mapping gap never hides resources that exist. + +#### Scenario: Only present resource types are named + +- **WHEN** the metamodel reports a service with state for only some of its resource types (e.g. SSM with `describeParameters` but no document/maintenance-window/association/patch-baseline operations) +- **THEN** the produced focus includes only the present resource types (e.g. only `Parameters`), not every registered type of that service + +#### Scenario: Drill-down lists live resources + +- **WHEN** the metamodel-derived focus is rendered and a present service/resource type is expanded in the Resources view +- **THEN** the providers list the actual resource ARNs from the emulator endpoint + +#### Scenario: Unmappable operation falls back to the full type set + +- **WHEN** the metamodel reports an API operation for a present service that maps to no known resource type +- **THEN** that service's resource types are included as before (the full set) and the unmapped operation is logged, rather than the service's resources being hidden + +### Requirement: Emulator unavailable handling + +The system SHALL handle the emulator being unavailable or the metamodel request failing by surfacing a non-fatal error state rather than crashing the view. + +#### Scenario: Emulator not running + +- **WHEN** the user selects `All Resources` under LocalStack Instances while the emulator is not reachable +- **THEN** the Resources view shows an error or empty state and the rest of the LocalStack view remains usable diff --git a/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/resource-browser/spec.md b/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/resource-browser/spec.md new file mode 100644 index 0000000..29a9a73 --- /dev/null +++ b/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/resource-browser/spec.md @@ -0,0 +1,184 @@ +## ADDED Requirements + +### Requirement: Resources view renders the active focus + +The system SHALL provide a "Resources" tree view that renders the active focus as a hierarchy of profile → region → **service-and-resource-type** → resource (ARN). The service and resource type SHALL be combined into a single row rather than two nested levels: the row's label is the service name and its description (dimmed) is the resource type's plural name (e.g. label `SQS` / description `Queues`). A service with multiple resource types SHALL render one row per resource type, each sharing the service name (e.g. `Lambda — Functions` and `Lambda — Event Source Mappings`). When no focus is active, the view SHALL show a placeholder prompting the user to select a focus. + +The combined service-and-resource-type row SHALL carry an icon that denotes the **target** of its profile, not the service: a LocalStack mark when the profile points at a LocalStack emulator, and a generic cloud icon when it points at real AWS. This icon SHALL appear on the combined row and SHALL NOT appear on the individual resource (ARN) leaves. A profile SHALL be treated as targeting LocalStack when it resolves to a custom/local endpoint (or is the synthetic `localstack` instance profile) and as targeting AWS otherwise. The system SHALL NOT bundle or display AWS-derived service icons. + +#### Scenario: No focus shows a placeholder + +- **WHEN** no focus selector has been activated +- **THEN** the Resources view shows a placeholder prompting the user to select a focus + +#### Scenario: Active focus is rendered hierarchically + +- **WHEN** a focus is active +- **THEN** the Resources view shows its profiles, and each profile expands to regions, then to combined service-and-resource-type rows, then to resources + +#### Scenario: Service and resource type share one row + +- **WHEN** a region contains the SQS service with its single `Queues` resource type +- **THEN** a single row labeled `SQS` with the dimmed description `Queues` is shown, and expanding it lists the queue ARNs (which carry no row icon) + +#### Scenario: A multi-resource-type service renders one row per type + +- **WHEN** a region contains Lambda (which has both Functions and Event Source Mappings) +- **THEN** two sibling rows appear — `Lambda` / `Functions` and `Lambda` / `Event Source Mappings` — each carrying the same target icon + +#### Scenario: Empty resource types still appear + +- **WHEN** a combined service-and-resource-type row has no resources +- **THEN** the row is still shown and expands to a `[ No Resources ]` placeholder + +#### Scenario: LocalStack-targeted rows show the LocalStack mark + +- **WHEN** a service-and-resource-type row belongs to a profile that targets a LocalStack emulator (a custom/local endpoint or the synthetic `localstack` instance profile) +- **THEN** the row's icon is the LocalStack mark, not an AWS-derived service icon + +#### Scenario: AWS-targeted rows show a generic cloud icon + +- **WHEN** a service-and-resource-type row belongs to a profile that targets real AWS (no custom endpoint) +- **THEN** the row's icon is a generic cloud icon, not an AWS-derived service icon + +### Requirement: Dynamic expansion of wildcard selectors + +The system SHALL expand wildcard and default selectors in the active focus when rendering: wildcard profiles to all configured profiles, wildcard/default regions to the appropriate region set, wildcard services to **all manifest services that have a registered curated provider**, and wildcard ARNs to the resources actually present (listed via the service's curated provider). The resource tree SHALL remain flat (profile → region → service-and-resource-type → resource); resource types that are conceptually nested under another resource are still listed as flat, region-wide rows. + +#### Scenario: Wildcard service node expands to curated services + +- **WHEN** a region in the focus uses a wildcard service selector +- **THEN** expanding the region lists every manifest service that has a registered curated provider, each with its display name and icon + +#### Scenario: Wildcard ARN node lists live resources + +- **WHEN** a resource type uses a wildcard ARN selector +- **THEN** expanding it lists the actual resources of that type from the platform via the service's curated provider, or a placeholder when none exist + +### Requirement: Resource Details view + +The system SHALL provide a "Resource Details" view, rendered as a **webview** showing a key/value **table** of the resource currently selected in the Resources view, including at least its ARN and service, plus service-specific fields. The system SHALL obtain these fields via the AWS SDK `describeResource` path for the selected resource's service — directed at the profile's `endpoint_url` when present — for both LocalStack and real AWS cloud resources, so behavior is identical across the two. The system SHALL NOT source Resource Details from the LocalStack metamodel. The webview SHALL be self-contained (a strict Content-Security-Policy, no external resources) and SHALL match the active VS Code theme via theme CSS variables; field values SHALL be HTML-escaped. When no resource is selected, it SHALL show a placeholder; when `describeResource` fails, it SHALL show an error in place of the fields. + +The table SHALL format values according to each field's `FieldType` (display only): `JSON` as a pretty-printed monospace block, `LONG_TEXT` as a wrapped monospace block, `ARN`/`LOG_GROUP` in a monospace cell, and `DATE`/`NUMBER`/`NAME`/`SHORT_TEXT` as plain text. Making these field types **interactive** (e.g. an `ARN` link that reveals the resource, a `LOG_GROUP` link, or "open in editor" for `JSON`/`LONG_TEXT`) is out of scope for this change and is deferred to future work; the webview foundation (which supports `command:` URIs and `postMessage`) is what enables it later. + +The field (left) column SHALL occupy at most one third (33%) of the view width; a field label longer than that SHALL word-wrap within the column rather than expand it, so the value (right) column always retains at least two thirds of the width. + +#### Scenario: Selecting a resource shows its details as a table + +- **WHEN** the user selects a resource (ARN) in the Resources view +- **THEN** the Resource Details webview shows a table of that resource's ARN, service, and service-specific fields, themed to match VS Code + +#### Scenario: Field types are formatted + +- **WHEN** a described field has type `JSON` or `LONG_TEXT` +- **THEN** its value is rendered in a monospace block (JSON pretty-printed), rather than a single truncated line + +#### Scenario: A long field label wraps within the capped column + +- **WHEN** a described field's label is longer than one third of the view width +- **THEN** the label word-wraps within the field column, the field column stays at most 33% wide, and the value column keeps at least two thirds of the width + +#### Scenario: Describe failure shows an error + +- **WHEN** `describeResource` throws for the selected resource +- **THEN** the Resource Details view shows an error message rather than a stale or empty table + +#### Scenario: Region-less resources can be described + +- **WHEN** the selected resource has a region-less ARN (e.g. an S3 bucket `arn:aws:s3:::name`) +- **THEN** the SDK client for the describe call uses the profile's default region (or a built-in default) rather than an empty region, so the details load instead of failing with "Region is missing" + +#### Scenario: Details come from the SDK for LocalStack and cloud alike + +- **WHEN** the selected resource belongs to a LocalStack instance (profile with an `endpoint_url`) +- **THEN** the Resource Details fields are fetched via the SDK `describeResource` call directed at that endpoint, using the same code path as a real-AWS-cloud resource + +#### Scenario: No selection shows a placeholder + +- **WHEN** no resource is selected in the Resources view +- **THEN** the Resource Details view shows a placeholder prompting the user to select a resource + +### Requirement: AWS platform service providers + +The system SHALL implement a curated AWS service provider for every service in the service manifest (see the `service-catalog` capability). Each provider SHALL list resource ARNs for a profile/region/resource-type and describe an individual resource's fields with a per-service custom field set. Providers MAY be authored declaratively or as imperative `ServiceProvider` subclasses; both SHALL talk to AWS via the AWS SDK using the profile's configuration, honoring a custom `endpoint_url` so the same providers work against the LocalStack emulator. There SHALL be no generic provider serving uncurated services. All AWS-specific code SHALL live under `src/platforms/aws/`. + +#### Scenario: Provider lists resources for a wildcard type + +- **WHEN** the Resources view expands a wildcard ARN node for a service with a registered provider +- **THEN** the corresponding AWS provider returns the live ARNs (or primary identifiers) for that profile, region, and resource type + +#### Scenario: Provider describes a selected resource + +- **WHEN** a resource of a curated service is selected +- **THEN** the corresponding AWS provider returns that resource's service-specific descriptive fields for the Resource Details view + +#### Scenario: Providers honor a custom endpoint + +- **WHEN** the selected profile defines an `endpoint_url` (e.g. the LocalStack endpoint) +- **THEN** the provider's SDK calls are directed to that endpoint + +### Requirement: CloudFormation stack focus + +The system SHALL compute a focus for a CloudFormation stack by querying the stack's resources and grouping them by service and resource type into the focus structure, so a `Stack: ` selector scopes the Resources view to exactly that stack's resources. Resources whose service maps to a registered curated provider SHALL be shown under that service and resource type. A resource SHALL be skipped only when it cannot be represented — its service has no registered provider yet, or a required identifying field is absent — and any such skip SHALL be logged rather than aborting the whole stack. + +#### Scenario: CFN selector scopes to stack resources + +- **WHEN** the user selects a `Stack: ` focus selector +- **THEN** the Resources view shows only the resources belonging to that CloudFormation stack, grouped by service and resource type + +#### Scenario: Resources of curated services appear + +- **WHEN** a CloudFormation stack contains resources whose service has a registered curated provider (e.g. `AWS::S3::Bucket` once S3 is curated) +- **THEN** those resources are shown under their service and resource type + +#### Scenario: Unrepresentable resources are skipped and logged + +- **WHEN** a stack resource cannot be represented (its service has no provider yet, or a required field is absent) +- **THEN** that single resource is skipped and the skip is logged, while every representable resource in the stack is still shown + +### Requirement: Profile node account label + +In the Resources view, the profile node's description SHALL show the AWS account ID and, when present, the account alias, formatted `( - )`. When the account alias is empty, the description SHALL show `()` only — with no trailing ` - ` separator. + +#### Scenario: Account alias is shown when present + +- **WHEN** the selected profile's account has an alias `my-org` +- **THEN** the profile node description reads `( - my-org)` + +#### Scenario: Empty alias omits the separator + +- **WHEN** the selected profile's account has no alias +- **THEN** the profile node description reads `()` with no trailing `-` + +### Requirement: Resources view reflects edits to the active view + +When the Resources view is showing a user-defined saved view (filter) and that view's definition is edited, the Resources view SHALL refresh to reflect the new definition without the user reselecting it. Saved-view focus selectors SHALL resolve their definition from current settings at selection/refresh time rather than from a snapshot captured when the tree was built. When the currently active saved view is removed, the Resources view SHALL revert to its no-focus placeholder. + +#### Scenario: Editing the active view updates the Resources view + +- **WHEN** a saved view is the active focus in the Resources view and the user edits that view's service/resource-type pairs +- **THEN** the Resources view refreshes to show the edited view's pairs without the user reselecting it + +#### Scenario: Removing the active view clears the Resources view + +- **WHEN** the active saved view is removed +- **THEN** the Resources view reverts to its placeholder prompting the user to select a focus + +### Requirement: Manual refresh of the Resources and Resource Details views + +The Resources and Resource Details views SHALL each provide a refresh action in the view's title bar. Invoking it SHALL re-fetch and re-render the view's current content — the Resources view against its active focus, and the Resource Details view against its currently selected resource — without requiring the user to re-select a focus or resource. Refreshing the Resources view SHALL **recompute** the active focus from its source rather than re-rendering a cached structure; for a LocalStack instance focus this means re-querying the metamodel API, so resources created since the focus was first selected appear. + +#### Scenario: Refreshing the Resources view re-fetches the active focus + +- **WHEN** the user clicks the refresh action in the Resources view title bar while a focus is active +- **THEN** the view re-queries the platform and re-renders the resources for that same focus + +#### Scenario: Refreshing a LocalStack instance focus picks up new resources + +- **WHEN** the active focus came from a LocalStack instance "All Resources" selector and a new resource has since been created in the emulator +- **THEN** clicking refresh re-queries the metamodel, recomputes the focus, and the new resource appears without the user re-selecting the focus selector + +#### Scenario: Refreshing the Resource Details view re-fetches the selected resource + +- **WHEN** the user clicks the refresh action in the Resource Details view title bar while a resource is selected +- **THEN** the view re-fetches and re-renders that resource's fields diff --git a/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/service-catalog/spec.md b/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/service-catalog/spec.md new file mode 100644 index 0000000..0c4c27a --- /dev/null +++ b/openspec/changes/archive/2026-06-23-integrate-resource-browser/specs/service-catalog/spec.md @@ -0,0 +1,71 @@ +## ADDED Requirements + +### Requirement: Static service manifest derived from published coverage + +The system SHALL maintain a static service manifest enumerating every service LocalStack publishes as supported, generated from LocalStack's coverage data (`localstack-docs/src/data/coverage/*.json`) and committed to the repository. Each manifest entry SHALL use the service's AWS service code as its id (e.g. `s3`, `secretsmanager`, `cognito-idp`, `logs`, `events`, `states`) and a display name. Every published service SHALL be included regardless of community/pro availability, and the system SHALL NOT store or display the availability distinction — because the browser also targets real AWS, all services are treated as fully available to everyone. The manifest SHALL be the single source of truth for which services exist, and SHALL be regenerated only on demand (when a developer notices it is out of date). The system SHALL NOT query a running emulator (e.g. `/_localstack/health`) or any discovery API (e.g. Cloud Control) to determine the supported set. + +#### Scenario: Manifest covers the full published service set + +- **WHEN** the manifest is loaded +- **THEN** it contains an entry for every service in LocalStack's published coverage data, keyed by the AWS service code, including services with no hand-written provider yet + +#### Scenario: Supported set is not discovered at runtime + +- **WHEN** the resource browser needs to know which services exist +- **THEN** it reads the static manifest and does not call `/_localstack/health`, Cloud Control, or any other runtime discovery endpoint + +### Requirement: Every supported service has a curated provider + +The system SHALL provide, for each manifest service, a curated provider that declares the service's resource types (each with singular/plural display names), lists the live resources for a profile/region/resource-type, and describes a single resource. There SHALL be no generic or fallback provider that serves arbitrary services without per-service curation. Provider resolution SHALL return the curated provider for a manifest service id. A manifest service that does not yet have a registered provider SHALL be absent from the browser rather than served generically. Completeness — every manifest service having a registered provider — is the definition of done and SHALL be enforced by an automated check. + +#### Scenario: Resolution returns the curated provider + +- **WHEN** a provider is resolved for a manifest service id that has a registered provider +- **THEN** that curated provider instance is returned + +#### Scenario: No generic fallback + +- **WHEN** a manifest service has no registered provider yet +- **THEN** the service is omitted from the resource browser, and no generic provider is used in its place + +#### Scenario: Completeness is enforced + +- **WHEN** the completeness check runs +- **THEN** it reports any manifest service id with no registered provider, and passes only when every manifest service has one + +### Requirement: Per-service detail fields + +Each curated provider SHALL define, per resource type, a fixed, ordered, typed subset of fields shown in the Resource Details view (field label, value path, and `FieldType`). This field set MAY be initially produced by a build-time generator that ranks the resource's API response members by importance, and SHALL be committed and hand-editable thereafter. At runtime the view SHALL render that selected subset — not the raw, unfiltered API response. + +#### Scenario: Detail fields are a selected, typed subset + +- **WHEN** a resource of a curated service is selected +- **THEN** the Resource Details view shows that resource type's defined field subset (labels, values, and types), not a generic dump of every response key + +#### Scenario: Field selection is authored, not computed at runtime + +- **WHEN** a provider's detail fields are determined +- **THEN** they come from the committed per-resource-type field spec (optionally generated by importance at build time and refined by hand), not from inspecting the response shape at runtime + +### Requirement: Declarative provider definitions + +The system SHALL support authoring curated providers as declarative definitions (data describing each resource type's list call and identifier, CloudFormation type mapping, and detail fields), executed by a shared engine that adapts them to the provider interface. The imperative `ServiceProvider` class SHALL remain available as an escape hatch for services that cannot be expressed declaratively. A declarative provider SHALL be functionally equivalent to an imperative one. + +#### Scenario: A declarative definition behaves as a provider + +- **WHEN** a service is authored as a declarative definition and resolved through the provider factory +- **THEN** it lists resource types, lists resources, and describes resources exactly as an imperative provider would + +#### Scenario: Escape hatch remains available + +- **WHEN** a service's behavior cannot be expressed in the declarative format +- **THEN** it MAY be implemented as an imperative `ServiceProvider` subclass and registered the same way + +### Requirement: Service-code mapping for emulator labels + +The system SHALL map service labels that appear in emulator data (metamodel labels, CloudFormation resource namespaces) to manifest service-code ids, case-insensitively with a documented override table for exceptions (e.g. `stepfunctions → states`, `StepFunctions → states`). Ids SHALL be the AWS service codes (e.g. `cognito-idp`). + +#### Scenario: Override mapping resolves exceptions + +- **WHEN** an emulator label does not equal a manifest id under a simple lowercase transform (e.g. Step Functions) +- **THEN** the documented override mapping resolves it to the correct manifest id diff --git a/openspec/changes/archive/2026-06-23-integrate-resource-browser/tasks.md b/openspec/changes/archive/2026-06-23-integrate-resource-browser/tasks.md new file mode 100644 index 0000000..37fe4af --- /dev/null +++ b/openspec/changes/archive/2026-06-23-integrate-resource-browser/tasks.md @@ -0,0 +1,153 @@ +## 1. Dependencies & scaffolding + +- [x] 1.1 Add `@aws-sdk/client-{account,cloudformation,dynamodb,iam,lambda,sfn,sns,sqs,sts}` and `js-ini` to `package.json`; run `pnpm install` +- [x] 1.2 Create the platform layout `src/platforms/aws/{clients,services,models}` and `src/views/{localstack,resources,resource-details}` +- [x] 1.3 Verify esbuild bundles the AWS SDK with no missing-module errors + +## 2. Platform-neutral Focus model + +- [x] 2.1 Implement `src/models/focus.ts` (zod `Focus` schema + types, `StandardModel`, `loadStandardModel`) following Toolkit conventions +- [x] 2.2 Add error and `memoize` utilities under `src/utils/` +- [x] 2.3 Add focus fixtures (`mock-*.focus.json`) and `focus.ts` tests + +## 3. AWS platform code + +- [x] 3.1 Implement `src/platforms/aws/models/{arnModel,awsConfig,cfnStackModel,regionModel}.ts` +- [x] 3.2 Implement the AWS SDK client wrappers under `src/platforms/aws/clients/`, routing every client constructor through `AWSConfig.getClientConfig()` so `endpoint_url` is honored (D4) +- [x] 3.3 Implement `src/platforms/aws/services/{serviceProvider,providerFactory}.ts` and each service's `provider.ts` +- [x] 3.4 Add the model tests (`arnModel`, `awsConfig`, `cfnStackModel`) under `src/test/` + +## 4. Resources & Resource Details views + +- [x] 4.1 Implement `src/views/resources/{viewProvider,treeItems}.ts` (wildcard/`default` expansion) +- [x] 4.2 Implement the Resource Details view under `src/views/resource-details/` +- [x] 4.3 Merge the service + resource-type levels into a single `ResourceServiceTypeTreeItem` (label = service name, dimmed description = plural type); one row per (service, resource-type); ARN leaves carry no icon (D11) +- [x] 4.4 Render Resource Details as a `WebviewViewProvider`: self-contained CSP-locked themed key/value table, per-`FieldType` formatting, HTML-escaped values, placeholder/error states; `setArn`/`refresh` re-fetch via `describeResource` (D11) + +## 5. Combined Explore view + +- [x] 5.1 `src/views/localstack/treeItems.ts`: node classes for the three sections, profile/region nodes, focus selectors, placeholders, and error nodes +- [x] 5.2 `src/views/localstack/viewProvider.ts`: render LocalStack Instances (instance node + `App Inspector` + `View: All Resources` + saved instance views), Cloud Profiles (profiles → regions → selectors/`Stack:`/views), and Workspace IaC (`Coming soon`) +- [x] 5.3 Reuse `localStackStatusTracker` and `localstack.openAppInspector`; add `getLocalStackEndpoint()` reading the `localstack` profile's `endpoint_url` (DNS-default fallback); label the instance node `AWS (): ` from it, status folded onto the line (D5) +- [x] 5.4 Fix container-name detection: `createContainerStatusTracker` accepts multiple names (`["localstack-main","localstack-aws"]`) for both the `docker inspect`/`ps` check and the `docker events` stream +- [x] 5.5 Build Cloud Profiles `View: All Resources` as a wildcard focus and `Stack: ` selectors via `cfnStackModel.toFocusModel()`; include the bundled `localstack` profile in Cloud Profiles +- [x] 5.6 Gate instance children on running state: `makeInstanceChildren()` returns `[]` unless running; a stopped instance renders as a plain non-expandable line +- [x] 5.7 Account label: `ResourceProfileTreeItem.description` = `( - )`, or `()` with no trailing ` - ` when the alias is empty + +## 6. LocalStack metamodel → Focus + +- [x] 6.1 Promote the captured `metamodel-sample.json` (account → Service → region → operation → response) into `src/test/resources/` as the translation fixture +- [x] 6.2 Map service label → manifest id via `toLowerCase()` + an override table (Step Functions → `states`, unverified — emulator does not yet emit Step Functions state); shared by the metamodel and CFN paths +- [x] 6.3 Implement `src/platforms/aws/models/metamodelFocus.ts`: fetch `/_localstack/pods/state/metamodel` (lenient parse for raw control chars), filter to account `000000000000`, transpose to Focus, map/filter to services with a registered provider, dedup the `""` global region, leave ARNs as wildcards +- [x] 6.4 Name only present resource types: annotate each resource type with its metamodel list-operation (`metamodelOp`); the engine builds an op→type map (imperative multi-type providers override directly); `metamodelToFocus` includes only types whose operation key appears, falling back to the full type set (logged) when an op is unmappable (D6) +- [x] 6.5 Wire the LocalStack Instances `View: All Resources` selector to `metamodelFocus`; handle emulator-unavailable / fetch-failure non-fatally; log present services dropped for lack of a provider +- [x] 6.6 Fixture-based tests: service mapping, unsupported-service filtering, `""`-region dedup, present-types-only (SSM `describeParameters` → only Parameters), unmapped-op fallback + +## 7. View wiring & plugin integration + +- [x] 7.1 New `src/plugins/resource-browser.ts` plugin registers the three views (`localstack.instances`, `localstack.resources`, `localstack.resourceDetails`) and initializes `ProviderFactory` +- [x] 7.2 Wire selection events: focus-selector selection → `computeFocus` → `ResourceViewProvider`; Resources selection → `ResourceDetailsViewProvider.setArn` +- [x] 7.3 Remove the old `InstancesTreeDataProvider` from `app-inspector-webview.ts` (keep the App Inspector command); register the new plugin in `extension.ts` +- [x] 7.4 `package.json` `contributes.views`: add `Resources` and `Resource Details` to `localstackActivityBar`; name the instances view "Explore"; relative `size` weights (Resources `2`, others `1`) for ~50/25/25 first layout +- [x] 7.5 Refresh on `statusTracker.onChange` (rebuild the instance label + App Inspector description) and on `onDidChangeConfiguration` for the view-state keys +- [x] 7.6 Add `localstack.refreshResources` / `localstack.refreshResourceDetails` (`$(refresh)`, `view/title` navigation group); the Resources refresh recomputes the focus via a stored focus **producer** so a LocalStack instance re-queries the metamodel and picks up new resources + +## 8. Settings persistence + +- [x] 8.1 Add a shared `configTarget()` (Workspace when a folder is open, else Global) and a JSON deep-clone helper (VS Code config objects are not structured-cloneable); use them for all view-state writes +- [x] 8.2 `localstack.cloudProfiles.regions`: `Select Regions...` multi-select replaces the user-added set in one action; the configured default region is always shown and never removable +- [x] 8.3 `localstack.cloudProfiles.shown` (opt-in): unset ⇒ `["default"]` (or the first profile when none is named `default`); `Select Profiles...` pre-checks and persists the shown set; an empty set shows a `No profiles selected` placeholder +- [x] 8.4 `localstack.cloudProfiles.filters`: per-filter value stores `{ name, resources: { service, resourceType }[], scope }`; the wizard lists service/resource-type pairs (`""`), validates name uniqueness, reserves `All Resources` (case-insensitive), and offers `This region only` / `All regions in this profile` +- [x] 8.5 `Add View...` / `Edit View...` (re-run wizard pre-populated) / `Remove View` (modal confirm); `Remove Region` (modal confirm) + +## 9. Single-select focus selectors + +- [x] 9.1 Register the instances tree once with `canSelectMany: false` and a single selection listener +- [x] 9.2 `computeFocus` returns the single selected focus selector's focus (`undefined` when nothing focusable is selected); no `mergeFocuses` (D8) + +## 10. Explore UX (final state) + +> The labels, icons, and affordances below are the landed result of several rounds of in-editor feedback. + +- [x] 10.1 Focus-selector labels: `View: All Resources`, saved views `View: `, CloudFormation `Stack: `; focus selectors carry a transparent `blank` icon so the instance's `View: All Resources` aligns under `App Inspector` (the only use of `blank`; other iconless rows render with no icon column) +- [x] 10.2 App Inspector node: `$(search)` icon, `Click to open` description (only rendered when running) +- [x] 10.3 Row actions are inline icons **and** right-click context items (D10): pencil for `Select Profiles...` / `Select Regions...` / `Edit View...`, plus for `Add View...`, trash for `Remove Region` / `Remove View` +- [x] 10.4 ESLint typing cleanup: remove the per-file `no-unsafe-*` override for the AWS platform modules and fix the underlying code with real types (typed SDK responses, zod parsing for dynamic JSON, a typed `memoize` generic) + +## 11. Target-aware icons (no AWS-derived art) + +- [x] 11.1 Delete the 6 AWS-derived service SVGs in `resources/icons/services/`; remove `ServiceProvider.getIconPath` with its `fs`/`path` imports and `symbol-misc` fallback (and the orphaned `context` plumbing) +- [x] 11.2 Resolve `isLocalStack` per profile in `makeResourceProfiles` (custom/local `endpoint_url`, or the synthetic `localstack` profile ⇒ LocalStack; otherwise AWS; never throws); store it on `ResourceProfileTreeItem` +- [x] 11.3 `ResourceServiceTypeTreeItem` sets `iconPath` to `ThemeIcon("localstack-logo")` (LocalStack) or `ThemeIcon("cloud")` (AWS) via the parent chain; ARN leaves keep no icon +- [x] 11.4 Test: a LocalStack-targeted row resolves to `localstack-logo` and an AWS-targeted row to `cloud`; `getEndpointForProfile` tests + +## 12. Service manifest (static, coverage-derived) + +- [x] 12.1 Checked-in generator reads `localstack-docs/src/data/coverage/*.json` and emits `resources/service-manifest.json` (one `{ id, name }` per service, AWS service-code id, every service, availability neither stored nor shown); regeneration on demand only +- [x] 12.2 Commit the generated manifest; ensure esbuild/packaging bundles it +- [x] 12.3 Memoized, validated loader (`getManifest()`, `getEntry(id)`, `getAllServiceIds()`) +- [x] 12.4 Service-code label mapping (lowercase + override table, e.g. `stepfunctions → states`) shared by the metamodel and CFN paths +- [x] 12.5 Unit-test manifest loading and label mapping (incl. `cognito-idp` and the Step Functions override) + +## 13. Declarative provider engine + +- [x] 13.1 Define the format `defineService(id, name, { : { singular, plural, list, id, cfn, detailFrom, detail, metamodelOp } })` +- [x] 13.2 Engine adapts a definition to `ServiceProvider` (resource types, `getResourceArns`, `describeResource` by walking each field `path`, CFN mapping, op→type map), routing SDK calls through `getClientConfig` +- [x] 13.3 Keep the imperative `ServiceProvider` class as the escape hatch; both register through `ProviderFactory` identically +- [x] 13.4 Unit-test the engine with a stubbed SDK: resource-type listing, identifier mapping, path-based detail rendering, CFN mapping + +## 14. Detail-field generator (build-time) + +- [x] 14.1 Dev-time generator reads a resource type's Describe/Get (or list-item) shape from offline AWS API models (`aws-sdk` v2 `apis/*.normal.json` / botocore `service-2.json`; generator-only, not bundled) +- [x] 14.2 Importance heuristic (identifiers/names → status → type → timestamps → scalars; collapse/drop nested; exclude metadata/pagination/blobs; cap ~12) + `FieldType` mapping +- [x] 14.3 Emit `detail: [{ label, path, type }]` into the definitions; committed and hand-editable + +## 15. Manifest-backed provider registration + completeness + +- [x] 15.1 `ProviderFactory` registers curated/declarative providers and resolves by manifest id; a manifest service with no provider is absent (no generic fallback) +- [x] 15.2 Completeness test reports manifest ids with no registered provider (runs as a coverage tracker; green = done) +- [x] 15.3 Default-icon path for services without a bundled icon + +## 16. Batch 1 — curated providers (flat, ≤5 resource types) + +- [x] 16.1 S3 (`s3`): `Bucket` +- [x] 16.2 API Gateway (`apigateway`): `RestApi`, `Stage`, `ApiKey`, `UsagePlan`, `Authorizer` +- [x] 16.3 SSM (`ssm`): `Parameter`, `Document`, `MaintenanceWindow`, `Association`, `PatchBaseline` +- [x] 16.4 Secrets Manager (`secretsmanager`): `Secret` +- [x] 16.5 Kinesis (`kinesis`): `Stream`, `StreamConsumer` +- [x] 16.6 CloudWatch Logs (`logs`): `LogGroup`, `LogStream`, `MetricFilter`, `SubscriptionFilter`, `Destination` +- [x] 16.7 EventBridge (`events`): `EventBus`, `Rule`, `ApiDestination`, `Connection`, `Archive` +- [x] 16.8 KMS (`kms`): `Key`, `Alias` +- [~] 16.9 Cognito (`cognito-idp`): `UserPool`, `UserPoolClient`, `UserPoolGroup` done; `IdentityPool` deferred (it belongs to the separate `cognito-identity` service / SDK client) +- [x] 16.10 ECR (`ecr`): `Repository` +- [x] 16.11 Add the Batch 1 `@aws-sdk/client-*` dependencies and SDK client wrappers (pinned `3.901.0`) +- [x] 16.12 Generate first-cut detail fields per Batch 1 resource type and hand-refine +- [x] 16.13 Unit-test each Batch 1 provider's list mapping and detail fields with stubbed SDK responses + +## 17. Metamodel & CFN wiring for full coverage + +- [x] 17.1 `metamodelFocus` maps labels to manifest ids and includes present services with a registered provider; logs present-but-uncurated services +- [x] 17.2 `cfnStackModel` maps resources of curated services via the provider; skips + logs only unrepresentable / not-yet-curated resources (D16) +- [x] 17.3 Resources view leaf tolerates a non-ARN primary identifier (shows it verbatim) +- [x] 17.4 Update/extend `metamodelFocus` and `cfnStackModel` tests for the new mapping behavior + +## 18. Browser fixes + +- [x] 18.1 (#1 region-less describe) `AWSConfig.getClientConfig` falls back to `getRegionForProfile(profile)` then `us-east-1` when region is empty; listing unchanged; test added (D17) +- [x] 18.2 (#2 details column) `table-layout: fixed; td.field { width:33%; white-space:normal; overflow-wrap:break-word; } td.value { width:67% }` +- [x] 18.3 (#3 remove multi-select) remove the `localstack.focus.multiSelect` setting, the enable/disable commands + menus, the `localstack.multiSelect` context key, and `mergeFocuses` (+ its tests); single `canSelectMany:false` registration +- [x] 18.4 (#4 live edit refresh) saved-view focus producers resolve their definition live by (profile, name); add/edit/remove command handlers refresh the Resources view; removing the active view reverts to the placeholder; test added (D18) +- [x] 18.5 (#5 instance views) store instance views under the dedicated `localstack.instanceViews` key (no region scope); wizard omits the region-scope step for the instance entry point; focus = `computeMetamodelFocus` intersected with the chosen pairs; `Add View...` on the instance node, `Edit View...`/`Remove View` on instance view rows; test added (D19) + +## 19. Tests, docs & verification + +- [x] 19.1 `pnpm lint` + `pnpm run check-types` clean; full test suite passes +- [x] 19.2 Update `README.md` / `CHANGELOG.md` to describe the Resources and Resource Details views, full-service coverage, the manifest, and the declarative provider model +- [x] 19.3 Manual verification (foundation): instance `View: All Resources` (metamodel) with no "security token" error, Cloud Profiles drill-down, a `Stack:` selector, add/edit/remove view and region, target-aware icons under light/dark themes, and the refresh buttons all work; Resource Details populates — confirmed by the maintainer +- [ ] 19.4 Manual smoke (fixes + Batch 1): S3 details load; long labels wrap; no multi-select toggle; editing an active view refreshes Resources; `View: All Resources` shows only present resource types; instance views intersect the metamodel; Batch 1 services appear in "All Resources", drill down to live resources, show service-specific details, and appear in a `Stack:` listing — REQUIRES a running emulator / cloud profile + maintainer; not automatable here + +## 20. Remaining coverage (tracking) + +- [ ] 20.1 Subsequent batches (by popularity) until the completeness test (15.2) is green. After Batch 1, **17/116** manifest services have providers (7 imperative + 10 declarative); **99 remain**. Unskip the "DONE GATE" test in `providerCompleteness.test.ts` once coverage is complete +- [ ] 20.2 Add Cognito `IdentityPool` once the `cognito-identity` service (separate SDK client) is curated +- [ ] 20.3 Parity migration (deferred): re-express the 7 imperative providers (CloudFormation, DynamoDB, IAM, Lambda, SNS, SQS, Step Functions) as declarative definitions with parity tests, if/when desired — they remain the sanctioned escape hatch in the meantime diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..392946c --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,20 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours diff --git a/openspec/specs/focus-model/spec.md b/openspec/specs/focus-model/spec.md new file mode 100644 index 0000000..e7a3b4c --- /dev/null +++ b/openspec/specs/focus-model/spec.md @@ -0,0 +1,38 @@ +# focus-model Specification + +## Purpose +The `Focus` data structure — a hierarchy of profiles → regions → services → resource types → ARNs — that describes what the Resources view should show, including its wildcard (`*`) and `default` selector semantics. + +## Requirements +### Requirement: Focus data structure + +The system SHALL represent a "focus" as a hierarchical structure of profiles → regions → services → resource types → ARNs. A focus SHALL carry a `version` string and a list of profiles; each profile SHALL have an `id` and a list of regions; each region SHALL have an `id` and a list of services; each service SHALL have an `id` and a list of resource types; each resource type SHALL have an `id` and a list of ARN strings. The structure SHALL be validated against a schema before use. + +#### Scenario: Valid focus is accepted + +- **WHEN** a focus object matching the schema (version, profiles, regions, services, resource types, arns) is parsed +- **THEN** parsing succeeds and the typed focus object is returned + +#### Scenario: Malformed focus is rejected + +- **WHEN** a focus object missing required fields or with mistyped fields is parsed +- **THEN** parsing fails with a descriptive validation error and no focus is returned + +### Requirement: Wildcard and default selectors + +The system SHALL support wildcard (`*`) selectors for profiles, regions, services, resource types, and ARNs, and a `default` selector for regions. A wildcard at a level SHALL mean "expand dynamically to all available items at that level when rendered"; a `default` region SHALL mean "the profile's configured default region". Non-wildcard ids SHALL be used literally. + +#### Scenario: Wildcard service expands to all supported services + +- **WHEN** a region's services list contains exactly one service with id `*` +- **THEN** consumers SHALL expand it to every supported service provider, each with wildcard resource types and ARNs + +#### Scenario: Default region resolves to the profile's configured region + +- **WHEN** a profile's regions list contains exactly one region with id `default` +- **THEN** consumers SHALL resolve it to the region configured for that profile, or surface an error if none is configured + +#### Scenario: Wildcard ARN triggers live listing + +- **WHEN** a resource type's arns list is exactly `["*"]` +- **THEN** consumers SHALL list the actual ARNs from the platform rather than using literal values diff --git a/openspec/specs/localstack-explorer-view/spec.md b/openspec/specs/localstack-explorer-view/spec.md new file mode 100644 index 0000000..9da1de0 --- /dev/null +++ b/openspec/specs/localstack-explorer-view/spec.md @@ -0,0 +1,297 @@ +# localstack-explorer-view Specification + +## Purpose +The combined **Explore** tree view in the LocalStack activity-bar container: its LocalStack Instances / Cloud Profiles / Workspace IaC sections, the focus selectors that drive the Resources view, single-select behavior, and the user-added regions, saved views, instance views, and shown profiles persisted to settings. + +## Requirements +### Requirement: Combined Explore tree view + +The system SHALL provide a single tree view named "Explore" in the LocalStack activity-bar container with three top-level sections in this order: **LocalStack Instances**, **Cloud Profiles**, and **Workspace IaC**. The view name SHALL be "Explore" (not "LocalStack") to avoid visual duplication with the "LocalStack" activity-bar container title. No separator nodes SHALL be rendered between the sections. + +#### Scenario: Three sections are shown at the root + +- **WHEN** the Explore view is rendered +- **THEN** the three top-level section nodes appear in order — "LocalStack Instances", "Cloud Profiles", and "Workspace IaC" — with no separator node between them + +#### Scenario: View name avoids container duplication + +- **WHEN** the Explore view is rendered inside the "LocalStack" activity-bar container +- **THEN** the view's name reads "Explore", not "LocalStack" + +### Requirement: Initial view sizing + +When the LocalStack activity-bar container is first opened, the system SHALL allocate approximately 50% of the available vertical space to the **Resources** view and approximately 25% each to the **Explore** and **Resource Details** views, via relative `size` weights on the view contributions (Resources `2`, the other two `1`). This applies to first layout; the user's subsequent manual resizing persists and overrides these defaults. + +#### Scenario: Resources view gets half the height on first open + +- **WHEN** the LocalStack activity-bar container is opened for the first time +- **THEN** the Resources view occupies roughly half the vertical space and the Explore and Resource Details views occupy roughly a quarter each + +### Requirement: LocalStack Instances section + +Under **LocalStack Instances**, the system SHALL show an instance node labeled `AWS (): `, where the endpoint is derived from the endpoint the Toolkit is configured to use (the `localstack` profile's `endpoint_url` in `~/.aws/config`) rather than a hardcoded value, and `` is the live emulator status with its first letter capitalized (e.g. `Running`, `Stopped`). The status SHALL be presented on the same line as the endpoint, NOT as a separate `Status` child node. The instance node SHALL show child nodes only when the emulator is running: an `App Inspector` node that opens the App Inspector when clicked, a `View: All Resources` focus selector, and one `View: ` focus selector per user-defined view saved for the instance. When the emulator is not running, the instance node SHALL have no children and SHALL render as a non-expandable line (no twistie). + +The instance node SHALL offer an `Add View...` action (matching the Cloud Profiles region affordance); each instance `View: ` row SHALL offer `Edit View...` / `Remove View` actions. Instance views SHALL be persisted in a dedicated saved-view store (separate from the Cloud Profiles per-region views, to avoid colliding with the bundled `localstack` cloud profile) and SHALL NOT carry a region scope (the region-scope step is omitted for instance views). Selecting an instance view SHALL produce a focus that **intersects the live metamodel**: starting from the resources actually deployed in the running instance (the same source as `View: All Resources`) and keeping only the chosen service/resource-type pairs, so an instance view never lists a type with nothing deployed. + +#### Scenario: Stopped instance shows the status line and no children + +- **WHEN** the emulator is stopped and the endpoint is `localhost:4566` +- **THEN** the instance node is labeled `AWS (Stopped): localhost:4566`, with no separate `Status` child node, no `App Inspector`/`View: All Resources` children, and no expand twistie + +#### Scenario: Running instance exposes app inspector and a focus selector + +- **WHEN** the emulator is running and the LocalStack Instances section is expanded +- **THEN** the instance node shows an `App Inspector` child and a `View: All Resources` focus selector child (and no standalone `Status` child) + +#### Scenario: Children appear and disappear with live status + +- **WHEN** the emulator transitions from stopped to running (or vice versa) +- **THEN** the instance node gains (or loses) its `App Inspector` and `View: All Resources` children without a manual refresh + +#### Scenario: Instance label reflects the configured endpoint + +- **WHEN** the `localstack` profile's `endpoint_url` is `http://127.0.0.1:4566` (e.g. DNS rebind protection is active) +- **THEN** the instance node's `host:port` is `127.0.0.1:4566`, matching the endpoint the SDK and metamodel calls use, not a hardcoded `localhost.localstack.cloud:4566` + +#### Scenario: Status reflects the running emulator + +- **WHEN** the emulator status changes (e.g. stopped → running) +- **THEN** the instance node's label updates to the new capitalized status (e.g. `AWS (Running): …`) without requiring a manual refresh, and the `App Inspector` node's description updates accordingly (see App Inspector scenarios) + +#### Scenario: Status detects containers started under alternate names + +- **WHEN** the emulator container is started by the `lstk` CLI under the name `localstack-aws` (rather than `localstack-main`) +- **THEN** the status reflects `Running` once the emulator is healthy, because the status tracker watches all known emulator container names — `localstack-main` and `localstack-aws` — for both the initial `docker inspect`/`ps` check and the live `docker events` stream + +#### Scenario: Adding a view to the instance + +- **WHEN** the emulator is running and the user invokes `Add View...` on the instance node, names the view, and selects one or more service/resource-type pairs +- **THEN** a `View: ` focus selector appears under the instance node and is saved for the instance (no region-scope step is shown) + +#### Scenario: An instance view shows only deployed resources of the chosen types + +- **WHEN** the user selects an instance view whose chosen pairs include a resource type with nothing deployed in the running instance +- **THEN** the Resources view shows only the deployed resources among the chosen pairs, consistent with `View: All Resources`, and does not list the empty type + +### Requirement: App Inspector node + +The `App Inspector` node under an instance SHALL display a magnifying-glass icon (the `search` theme icon) and SHALL open the existing App Inspector webview when clicked. Because the node is only shown when the emulator is running, its description SHALL read `Click to open`. + +#### Scenario: App Inspector shows a magnifying-glass icon + +- **WHEN** the running instance node is expanded +- **THEN** the `App Inspector` node displays the `search` (magnifying glass) theme icon and a `Click to open` description + +#### Scenario: App Inspector opens the existing webview + +- **WHEN** the user clicks the `App Inspector` node +- **THEN** the existing App Inspector webview panel opens (behavior unchanged from before this change) + +### Requirement: Cloud Profiles section + +Under **Cloud Profiles**, the system SHALL show one node per AWS profile discovered in `~/.aws/config` (including `default` and the bundled `localstack` profile), labeled `AWS: `. Each profile SHALL show its configured default region plus any user-added regions; each region SHALL contain a `View All Resources` focus selector, the filters applicable to that region, and one `Stack: ` focus selector per active CloudFormation stack in that region. Region-level actions (`Add View...`, and `Remove Region` for user-added regions) are available on the region row as inline icons and in the right-click context menu, not as child action nodes. The profile's region set is managed from the profile row's `Select Regions...` action (an inline filter icon, also in the context menu), also not via a child action node. + +#### Scenario: Profiles are listed from AWS config + +- **WHEN** the Cloud Profiles section is expanded and `~/.aws/config` defines profiles `default` and `staging` +- **THEN** nodes `AWS: default` and `AWS: staging` appear + +#### Scenario: A region lists its focus selectors + +- **WHEN** a profile's region is expanded +- **THEN** it shows `View: All Resources`, the filters applicable to that region (each labeled `View: `), and one `Stack: ` selector per active CloudFormation stack (the `Add View...` action is on the region row, not a child node) + +#### Scenario: The bundled localstack profile is shown + +- **WHEN** `~/.aws/config` includes the bundled `localstack` profile +- **THEN** an `AWS: localstack` node appears under Cloud Profiles, in addition to the LocalStack Instances section + +#### Scenario: Invalid credentials surface an error node + +- **WHEN** a profile cannot be queried (e.g. invalid credentials) +- **THEN** an error node is shown for that profile rather than failing the whole view + +### Requirement: Select which Cloud Profiles are shown + +By default, only the profile named `default` SHALL be shown under Cloud Profiles; when no profile named `default` exists, the first discovered profile SHALL be shown instead. The set of shown profiles SHALL be persisted in `localstack.cloudProfiles.shown` (a list of profile names) and SHALL NOT modify `~/.aws/config`. When the setting is **unset**, the system SHALL apply the default-only behavior above; when it is set (including the empty list), the system SHALL honor it exactly. The Cloud Profiles section row SHALL offer a `Select Profiles...` action (an inline gear icon, also in the right-click context menu) that opens a multi-select picker of all discovered profiles, pre-checked with the currently shown set; confirming SHALL persist the checked names as the shown set. When the shown set is empty, the section SHALL render a single non-interactive `No profiles selected` placeholder. + +#### Scenario: Only the default profile is shown initially + +- **WHEN** the Cloud Profiles section is first rendered, `localstack.cloudProfiles.shown` is unset, and `~/.aws/config` defines `default` and `staging` +- **THEN** only `AWS: default` appears, and `AWS: staging` is hidden until explicitly enabled + +#### Scenario: First profile shown when none is named default + +- **WHEN** `localstack.cloudProfiles.shown` is unset and no profile is named `default` +- **THEN** the first discovered profile is shown + +#### Scenario: Enabling additional profiles + +- **WHEN** the user opens `Select Profiles...` and checks `staging` +- **THEN** `AWS: staging` appears under Cloud Profiles and the choice is saved to `localstack.cloudProfiles.shown` without changing `~/.aws/config` + +#### Scenario: Deselecting every profile shows a placeholder + +- **WHEN** the user opens `Select Profiles...` and unchecks every profile +- **THEN** the Cloud Profiles section shows a single `No profiles selected` placeholder, and the empty selection is honored (not reset to the default) + +#### Scenario: Shown profiles persist across reloads + +- **WHEN** the workspace is reloaded after changing which profiles are shown +- **THEN** the same set of profiles is shown + +### Requirement: Settings persistence target + +All view-state writes (added regions, saved filters/views, instance views, and shown profiles) SHALL persist to Workspace settings when a workspace folder is open, and SHALL fall back to Global (User) settings when no workspace folder is open. Writing SHALL NOT fail when no folder is open. + +#### Scenario: Persists per-workspace when a folder is open + +- **WHEN** a workspace folder is open and the user changes a view-state setting (e.g. hides a profile) +- **THEN** the change is written to Workspace settings and applies to that workspace + +#### Scenario: Falls back to global with no folder open + +- **WHEN** no workspace folder is open and the user changes a view-state setting +- **THEN** the change is written to Global (User) settings without error, rather than failing with "Unable to write to Workspace Settings" + +### Requirement: Workspace IaC placeholder + +Under **Workspace IaC**, the system SHALL show a single placeholder child with the literal text `Coming soon`. + +#### Scenario: Placeholder is shown + +- **WHEN** the Workspace IaC section is expanded +- **THEN** a single non-interactive `Coming soon` node is shown + +### Requirement: Row actions are shown as inline icons and in the context menu + +The per-row actions in the Explore view SHALL be shown as inline action icons on the row (a hover toolbar) AND remain available in the row's right-click context menu. (VS Code only renders a tree row's `...` overflow alongside an inline action, so a row with no inline action has no `...` button at all; the actions are therefore contributed inline so they are always visible.) The inline icons SHALL be: an edit (pencil) icon for `Select Profiles...` (Cloud Profiles section row) and `Select Regions...` (profile row), a plus icon for `Add View...` (region rows and the instance node), a trash icon for `Remove Region` (user-added region rows), and a pencil icon for `Edit View...` plus a trash icon for `Remove View` (saved-view rows). Each of these SHALL also be contributed as a non-inline `view/item/context` item so it appears in the right-click menu. + +#### Scenario: Row actions show an inline icon + +- **WHEN** the user hovers the Cloud Profiles section row, a profile row, a region row, a user-added region row, or a saved-view row +- **THEN** the corresponding inline icon(s) are shown (pencil; pencil; plus; trash; pencil and trash respectively) and clicking one invokes the action + +#### Scenario: Inline actions are also in the context menu + +- **WHEN** the user right-clicks a row that has an inline action +- **THEN** the same action(s) are also listed in the context menu + +### Requirement: Select Regions + +The system SHALL let the user choose which regions are shown under a Cloud Profile via a `Select Regions...` action on the profile row (an inline filter icon, also in the right-click context menu) — not via a separate child node. The action opens a multi-select picker of all known regions (excluding the profile's configured default region, which is always shown). The picker SHALL be pre-checked with the regions currently shown for that profile; confirming SHALL replace the set of user-added regions (adding newly-checked and removing unchecked ones in a single action). The chosen regions SHALL be persisted per profile so they reappear on reload. + +#### Scenario: Select Regions lives on the profile row + +- **WHEN** the user views a profile row (e.g. `AWS: default`) +- **THEN** a `Select Regions...` action is available as an inline filter icon and in the right-click context menu, and there is no separate `Show more regions` child node under the profile + +#### Scenario: Selecting multiple regions persists them + +- **WHEN** the user invokes `Select Regions...` for a profile and checks several regions +- **THEN** all checked regions appear under that profile and are saved to settings + +#### Scenario: Deselecting removes a region + +- **WHEN** the user invokes `Select Regions...` and unchecks a currently-shown region +- **THEN** that region is removed from the profile (the configured default region is never offered for deselection) + +#### Scenario: Added regions survive reload + +- **WHEN** the workspace is reloaded after regions were added +- **THEN** the previously added regions are still shown under that profile + +### Requirement: Remove a region + +The system SHALL let the user remove a user-added region via a `Remove Region` action on that region row (an inline trash icon, also in the right-click context menu), removing only that one region from the workspace settings. Invoking the action SHALL first present a modal confirmation dialog; the region SHALL be removed only when the user confirms. The profile's configured default region SHALL always be shown and SHALL NOT offer a remove action. + +#### Scenario: Removing a user-added region + +- **WHEN** the user invokes Remove on a user-added region node and confirms the dialog +- **THEN** that single region is removed from the profile and from workspace settings, and other regions remain + +#### Scenario: Cancelling the confirmation keeps the region + +- **WHEN** the user invokes Remove on a user-added region node and cancels the dialog +- **THEN** the region is not removed and settings are unchanged + +#### Scenario: Default region cannot be removed + +- **WHEN** the user views the profile's configured default region node +- **THEN** no remove action is offered for it + +### Requirement: Add new view + +The system SHALL let the user define a named filter via the `Add View...` action — invoked from a region row's inline plus icon (also in the right-click context menu) and from the LocalStack Instance node — by selecting a set of **service / resource-type pairs** (e.g. `SQS — Queues`, `Lambda — Functions`), not whole services. This lets a view include some resource types of a service while excluding others. In the UX a saved filter is labeled a "view" (the actions are `Add View...`, `Edit View...`, `Remove View`); the underlying concept and the `localstack.cloudProfiles.filters` settings key are unchanged (the per-filter value stores a list of `{ service, resourceType }` pairs), and the remainder of this spec refers to the concept as a "filter" for continuity with the persisted model. For a **Cloud Profiles** region, a filter is by default scoped to the single region it was created under; the dialog SHALL offer an "apply to all regions" option that instead makes the filter available under every region of that profile. For a **LocalStack Instance** view, the region-scope step SHALL be omitted (instance views always apply to the running instance) and the view is stored in the dedicated instance-view store. The filter SHALL appear as a focus selector labeled `View: `, scoped to the chosen pairs, and SHALL be persisted with its scope. The name SHALL be unique within the profile (or instance) and SHALL NOT be `All Resources` (case-insensitive), which is reserved for the built-in all-resources selector; the dialog SHALL reject a reserved or duplicate name. + +#### Scenario: Creating a region-scoped filter + +- **WHEN** the user invokes `Add View...` from a region, names it, selects one or more service/resource-type pairs, and leaves "apply to all regions" off +- **THEN** a new focus selector labeled `View: ` appears under that region only and is saved with a single-region scope + +#### Scenario: The reserved name is rejected + +- **WHEN** the user enters `All Resources` (in any letter case) as the view name +- **THEN** the dialog rejects it as a reserved view name and does not save a filter + +#### Scenario: Creating a filter applied to all regions + +- **WHEN** the user invokes `Add View...`, selects pairs, and enables "apply to all regions" +- **THEN** the filter appears under every region of that profile and is saved with an all-regions scope + +#### Scenario: Creating an instance view omits the region-scope step + +- **WHEN** the user invokes `Add View...` on the LocalStack Instance node and selects pairs +- **THEN** no region-scope choice is presented and the view is saved for the instance + +#### Scenario: Selecting a filter scopes the Resources view to its resource types + +- **WHEN** the user selects a saved filter focus selector +- **THEN** the Resources view shows only the service/resource-type pairs chosen for that filter + +### Requirement: Edit and remove filters + +The system SHALL let the user edit or remove a saved filter via `Edit View...` / `Remove View` actions on the filter row (an inline gear icon for `Edit View...` and a trash icon for `Remove View`, also in the right-click context menu), for both Cloud Profiles region views and LocalStack Instance views. The `Edit View...` icon SHALL be the same gear (settings) icon used by `Select Profiles...` / `Select Regions...`. Editing SHALL reopen the filter wizard pre-populated with the filter's current name, services, and scope, and overwrite the saved filter with the new values. Removing SHALL first present a modal confirmation dialog and SHALL delete the filter from the workspace settings only when the user confirms. After a successful add, edit, or remove, the system SHALL refresh the Resources view so that, when the affected view is the active focus, the Resources view reflects the change (or reverts to its placeholder if the active view was removed) without the user reselecting it. + +#### Scenario: Editing a filter updates it + +- **WHEN** the user invokes Edit on a filter, changes its services, and confirms +- **THEN** the filter's saved services are updated and the focus selector reflects the new scope + +#### Scenario: Editing the active filter refreshes the Resources view + +- **WHEN** the user edits the filter that is currently the active focus +- **THEN** the Resources view refreshes to reflect the new definition without the user reselecting the view + +#### Scenario: Removing a filter deletes it + +- **WHEN** the user invokes Remove on a filter and confirms the dialog +- **THEN** the filter no longer appears under any region (or under the instance) and is deleted from workspace settings + +#### Scenario: Cancelling the confirmation keeps the filter + +- **WHEN** the user invokes Remove on a filter and cancels the dialog +- **THEN** the filter is left unchanged + +### Requirement: Focus selectors drive the active focus + +The system SHALL treat `View: All Resources`, saved filters (`View: `), and `Stack: ` nodes as focus selectors. Selecting a focus selector SHALL compute its focus and set it as the active focus for the Resources view. Focus selectors SHALL carry a transparent (`blank`) icon so their labels align with icon-bearing sibling rows — specifically so the instance's `View: All Resources` selector lines up with the `App Inspector` node. This is the only place a `blank` icon is used; other iconless rows render without one. + +#### Scenario: Selecting a focus selector updates the Resources view + +- **WHEN** the user clicks a focus selector +- **THEN** the Resources view re-renders to show the resources described by that selector's focus + +### Requirement: Single-select behavior + +The system SHALL use single-select for focus selectors: activating one focus selector SHALL deactivate any other, and the Resources view SHALL reflect exactly the most recently selected focus. The system SHALL NOT provide a multi-select mode or a toggle for one. + +#### Scenario: Selecting a focus selector replaces the active focus + +- **WHEN** the user selects a focus selector and then selects a different one +- **THEN** only the most recently selected focus is active and the Resources view reflects it alone + +#### Scenario: No multi-select toggle is present + +- **WHEN** the user opens the LocalStack view's `...` title menu +- **THEN** no enable/disable multi-select action is shown diff --git a/openspec/specs/localstack-metamodel/spec.md b/openspec/specs/localstack-metamodel/spec.md new file mode 100644 index 0000000..fb109e9 --- /dev/null +++ b/openspec/specs/localstack-metamodel/spec.md @@ -0,0 +1,75 @@ +# localstack-metamodel Specification + +## Purpose +Translating the running emulator's `/_localstack/pods/state/metamodel` endpoint into a `Focus` for the LocalStack Instances section — naming the services and resource types actually present, scoped to the bundled LocalStack profile. + +## Requirements +### Requirement: Compute a focus from the emulator metamodel + +The `All Resources` selector under **LocalStack Instances** SHALL compute its focus from the running emulator's `/_localstack/pods/state/metamodel` endpoint, reached at the endpoint the Toolkit is configured to use (the `localstack` profile's `endpoint_url`) rather than a hardcoded URL. The payload is nested as account → service label → region → API operation → raw response. The system SHALL fetch that endpoint and translate (transpose) its JSON into a Focus that names the services and regions actually present in the emulator state, scoped to the bundled LocalStack profile. + +#### Scenario: Metamodel is translated into a focus + +- **WHEN** the user selects `All Resources` under LocalStack Instances and the emulator is running +- **THEN** the system fetches `/_localstack/pods/state/metamodel` and produces a focus listing the services and regions present in the returned state, with the service and region axes transposed into Focus order (profile → region → service) + +#### Scenario: Only present services are named + +- **WHEN** the metamodel reports state for a subset of services +- **THEN** the produced focus includes only those services (not every supported service) + +### Requirement: Service-label mapping and unsupported-service filtering + +The metamodel labels services in PascalCase (e.g. `CloudFormation`, `IAM`, `S3`). The system SHALL map each label to its manifest service-code id (case-insensitive, with a documented override table for exceptions such as Step Functions). Every service present in the metamodel that resolves to a manifest service **with a registered curated provider** SHALL be included in the produced focus — this includes services beyond the original seven, as their providers are added. The metamodel SHALL be used only to determine which services are *present* in the running instance; it SHALL NOT be used to determine which services are *supported* (that comes from the static manifest, see the `service-catalog` capability). A present service that resolves to no registered provider (e.g. not yet curated during rollout) SHALL be omitted and the omission SHALL be surfaced (e.g. via logging). + +#### Scenario: PascalCase labels map to manifest ids + +- **WHEN** the metamodel contains `CloudFormation` and `IAM` +- **THEN** the produced focus contains services with ids `cloudformation` and `iam` + +#### Scenario: A present service with a curated provider appears + +- **WHEN** the metamodel contains a service that has a registered curated provider (e.g. `S3` once S3 is curated) +- **THEN** that service is included in the produced focus + +#### Scenario: A present but not-yet-curated service is omitted and logged + +- **WHEN** the metamodel contains a service that resolves to no registered provider +- **THEN** that service is omitted from the focus and the omission is logged + +### Requirement: Global-region and account handling + +The system SHALL treat an empty-string region (`""`, the global-service mirror) as a duplicate of the same service's regional entries and SHALL NOT emit a region node with an empty id, while still representing a service that appears only under `""`. The system MAY assume a single account maps to the bundled LocalStack profile. + +#### Scenario: Empty-string region is not rendered as a region node + +- **WHEN** a service appears under both `us-east-1` and `""` with identical contents +- **THEN** the produced focus contains that service once under `us-east-1` and no region node with an empty id + +### Requirement: Live resource listing under the metamodel focus + +The focus produced from the metamodel SHALL name only the resource types actually present in the metamodel for each service/region — determined from the metamodel's API-operation keys (each resource type maps to the list operation that signals its presence, e.g. SSM `describeParameters` → Parameters) — rather than expanding every present service to all of its registered resource types. The focus SHALL leave **ARN** selectors as wildcards so the AWS service providers list the live resource ARNs from the emulator on drill-down; only the resource-type axis is narrowed by the metamodel, not the ARN axis. When the metamodel reports an operation that cannot be mapped to a known resource type (or a resource type declares no operation), the system SHALL fall back to including that service's full resource-type set and SHALL log the gap, so a mapping gap never hides resources that exist. + +#### Scenario: Only present resource types are named + +- **WHEN** the metamodel reports a service with state for only some of its resource types (e.g. SSM with `describeParameters` but no document/maintenance-window/association/patch-baseline operations) +- **THEN** the produced focus includes only the present resource types (e.g. only `Parameters`), not every registered type of that service + +#### Scenario: Drill-down lists live resources + +- **WHEN** the metamodel-derived focus is rendered and a present service/resource type is expanded in the Resources view +- **THEN** the providers list the actual resource ARNs from the emulator endpoint + +#### Scenario: Unmappable operation falls back to the full type set + +- **WHEN** the metamodel reports an API operation for a present service that maps to no known resource type +- **THEN** that service's resource types are included as before (the full set) and the unmapped operation is logged, rather than the service's resources being hidden + +### Requirement: Emulator unavailable handling + +The system SHALL handle the emulator being unavailable or the metamodel request failing by surfacing a non-fatal error state rather than crashing the view. + +#### Scenario: Emulator not running + +- **WHEN** the user selects `All Resources` under LocalStack Instances while the emulator is not reachable +- **THEN** the Resources view shows an error or empty state and the rest of the LocalStack view remains usable diff --git a/openspec/specs/resource-browser/spec.md b/openspec/specs/resource-browser/spec.md new file mode 100644 index 0000000..93d5f70 --- /dev/null +++ b/openspec/specs/resource-browser/spec.md @@ -0,0 +1,188 @@ +# resource-browser Specification + +## Purpose +The **Resources** and **Resource Details** views driven by the active focus, the target-aware (LocalStack vs AWS) row icon, and the AWS platform service providers that list and describe resources via the AWS SDK against a profile's endpoint. + +## Requirements +### Requirement: Resources view renders the active focus + +The system SHALL provide a "Resources" tree view that renders the active focus as a hierarchy of profile → region → **service-and-resource-type** → resource (ARN). The service and resource type SHALL be combined into a single row rather than two nested levels: the row's label is the service name and its description (dimmed) is the resource type's plural name (e.g. label `SQS` / description `Queues`). A service with multiple resource types SHALL render one row per resource type, each sharing the service name (e.g. `Lambda — Functions` and `Lambda — Event Source Mappings`). When no focus is active, the view SHALL show a placeholder prompting the user to select a focus. + +The combined service-and-resource-type row SHALL carry an icon that denotes the **target** of its profile, not the service: a LocalStack mark when the profile points at a LocalStack emulator, and a generic cloud icon when it points at real AWS. This icon SHALL appear on the combined row and SHALL NOT appear on the individual resource (ARN) leaves. A profile SHALL be treated as targeting LocalStack when it resolves to a custom/local endpoint (or is the synthetic `localstack` instance profile) and as targeting AWS otherwise. The system SHALL NOT bundle or display AWS-derived service icons. + +#### Scenario: No focus shows a placeholder + +- **WHEN** no focus selector has been activated +- **THEN** the Resources view shows a placeholder prompting the user to select a focus + +#### Scenario: Active focus is rendered hierarchically + +- **WHEN** a focus is active +- **THEN** the Resources view shows its profiles, and each profile expands to regions, then to combined service-and-resource-type rows, then to resources + +#### Scenario: Service and resource type share one row + +- **WHEN** a region contains the SQS service with its single `Queues` resource type +- **THEN** a single row labeled `SQS` with the dimmed description `Queues` is shown, and expanding it lists the queue ARNs (which carry no row icon) + +#### Scenario: A multi-resource-type service renders one row per type + +- **WHEN** a region contains Lambda (which has both Functions and Event Source Mappings) +- **THEN** two sibling rows appear — `Lambda` / `Functions` and `Lambda` / `Event Source Mappings` — each carrying the same target icon + +#### Scenario: Empty resource types still appear + +- **WHEN** a combined service-and-resource-type row has no resources +- **THEN** the row is still shown and expands to a `[ No Resources ]` placeholder + +#### Scenario: LocalStack-targeted rows show the LocalStack mark + +- **WHEN** a service-and-resource-type row belongs to a profile that targets a LocalStack emulator (a custom/local endpoint or the synthetic `localstack` instance profile) +- **THEN** the row's icon is the LocalStack mark, not an AWS-derived service icon + +#### Scenario: AWS-targeted rows show a generic cloud icon + +- **WHEN** a service-and-resource-type row belongs to a profile that targets real AWS (no custom endpoint) +- **THEN** the row's icon is a generic cloud icon, not an AWS-derived service icon + +### Requirement: Dynamic expansion of wildcard selectors + +The system SHALL expand wildcard and default selectors in the active focus when rendering: wildcard profiles to all configured profiles, wildcard/default regions to the appropriate region set, wildcard services to **all manifest services that have a registered curated provider**, and wildcard ARNs to the resources actually present (listed via the service's curated provider). The resource tree SHALL remain flat (profile → region → service-and-resource-type → resource); resource types that are conceptually nested under another resource are still listed as flat, region-wide rows. + +#### Scenario: Wildcard service node expands to curated services + +- **WHEN** a region in the focus uses a wildcard service selector +- **THEN** expanding the region lists every manifest service that has a registered curated provider, each with its display name and icon + +#### Scenario: Wildcard ARN node lists live resources + +- **WHEN** a resource type uses a wildcard ARN selector +- **THEN** expanding it lists the actual resources of that type from the platform via the service's curated provider, or a placeholder when none exist + +### Requirement: Resource Details view + +The system SHALL provide a "Resource Details" view, rendered as a **webview** showing a key/value **table** of the resource currently selected in the Resources view, including at least its ARN and service, plus service-specific fields. The system SHALL obtain these fields via the AWS SDK `describeResource` path for the selected resource's service — directed at the profile's `endpoint_url` when present — for both LocalStack and real AWS cloud resources, so behavior is identical across the two. The system SHALL NOT source Resource Details from the LocalStack metamodel. The webview SHALL be self-contained (a strict Content-Security-Policy, no external resources) and SHALL match the active VS Code theme via theme CSS variables; field values SHALL be HTML-escaped. When no resource is selected, it SHALL show a placeholder; when `describeResource` fails, it SHALL show an error in place of the fields. + +The table SHALL format values according to each field's `FieldType` (display only): `JSON` as a pretty-printed monospace block, `LONG_TEXT` as a wrapped monospace block, `ARN`/`LOG_GROUP` in a monospace cell, and `DATE`/`NUMBER`/`NAME`/`SHORT_TEXT` as plain text. Making these field types **interactive** (e.g. an `ARN` link that reveals the resource, a `LOG_GROUP` link, or "open in editor" for `JSON`/`LONG_TEXT`) is out of scope for this change and is deferred to future work; the webview foundation (which supports `command:` URIs and `postMessage`) is what enables it later. + +The field (left) column SHALL occupy at most one third (33%) of the view width; a field label longer than that SHALL word-wrap within the column rather than expand it, so the value (right) column always retains at least two thirds of the width. + +#### Scenario: Selecting a resource shows its details as a table + +- **WHEN** the user selects a resource (ARN) in the Resources view +- **THEN** the Resource Details webview shows a table of that resource's ARN, service, and service-specific fields, themed to match VS Code + +#### Scenario: Field types are formatted + +- **WHEN** a described field has type `JSON` or `LONG_TEXT` +- **THEN** its value is rendered in a monospace block (JSON pretty-printed), rather than a single truncated line + +#### Scenario: A long field label wraps within the capped column + +- **WHEN** a described field's label is longer than one third of the view width +- **THEN** the label word-wraps within the field column, the field column stays at most 33% wide, and the value column keeps at least two thirds of the width + +#### Scenario: Describe failure shows an error + +- **WHEN** `describeResource` throws for the selected resource +- **THEN** the Resource Details view shows an error message rather than a stale or empty table + +#### Scenario: Region-less resources can be described + +- **WHEN** the selected resource has a region-less ARN (e.g. an S3 bucket `arn:aws:s3:::name`) +- **THEN** the SDK client for the describe call uses the profile's default region (or a built-in default) rather than an empty region, so the details load instead of failing with "Region is missing" + +#### Scenario: Details come from the SDK for LocalStack and cloud alike + +- **WHEN** the selected resource belongs to a LocalStack instance (profile with an `endpoint_url`) +- **THEN** the Resource Details fields are fetched via the SDK `describeResource` call directed at that endpoint, using the same code path as a real-AWS-cloud resource + +#### Scenario: No selection shows a placeholder + +- **WHEN** no resource is selected in the Resources view +- **THEN** the Resource Details view shows a placeholder prompting the user to select a resource + +### Requirement: AWS platform service providers + +The system SHALL implement a curated AWS service provider for every service in the service manifest (see the `service-catalog` capability). Each provider SHALL list resource ARNs for a profile/region/resource-type and describe an individual resource's fields with a per-service custom field set. Providers MAY be authored declaratively or as imperative `ServiceProvider` subclasses; both SHALL talk to AWS via the AWS SDK using the profile's configuration, honoring a custom `endpoint_url` so the same providers work against the LocalStack emulator. There SHALL be no generic provider serving uncurated services. All AWS-specific code SHALL live under `src/platforms/aws/`. + +#### Scenario: Provider lists resources for a wildcard type + +- **WHEN** the Resources view expands a wildcard ARN node for a service with a registered provider +- **THEN** the corresponding AWS provider returns the live ARNs (or primary identifiers) for that profile, region, and resource type + +#### Scenario: Provider describes a selected resource + +- **WHEN** a resource of a curated service is selected +- **THEN** the corresponding AWS provider returns that resource's service-specific descriptive fields for the Resource Details view + +#### Scenario: Providers honor a custom endpoint + +- **WHEN** the selected profile defines an `endpoint_url` (e.g. the LocalStack endpoint) +- **THEN** the provider's SDK calls are directed to that endpoint + +### Requirement: CloudFormation stack focus + +The system SHALL compute a focus for a CloudFormation stack by querying the stack's resources and grouping them by service and resource type into the focus structure, so a `Stack: ` selector scopes the Resources view to exactly that stack's resources. Resources whose service maps to a registered curated provider SHALL be shown under that service and resource type. A resource SHALL be skipped only when it cannot be represented — its service has no registered provider yet, or a required identifying field is absent — and any such skip SHALL be logged rather than aborting the whole stack. + +#### Scenario: CFN selector scopes to stack resources + +- **WHEN** the user selects a `Stack: ` focus selector +- **THEN** the Resources view shows only the resources belonging to that CloudFormation stack, grouped by service and resource type + +#### Scenario: Resources of curated services appear + +- **WHEN** a CloudFormation stack contains resources whose service has a registered curated provider (e.g. `AWS::S3::Bucket` once S3 is curated) +- **THEN** those resources are shown under their service and resource type + +#### Scenario: Unrepresentable resources are skipped and logged + +- **WHEN** a stack resource cannot be represented (its service has no provider yet, or a required field is absent) +- **THEN** that single resource is skipped and the skip is logged, while every representable resource in the stack is still shown + +### Requirement: Profile node account label + +In the Resources view, the profile node's description SHALL show the AWS account ID and, when present, the account alias, formatted `( - )`. When the account alias is empty, the description SHALL show `()` only — with no trailing ` - ` separator. + +#### Scenario: Account alias is shown when present + +- **WHEN** the selected profile's account has an alias `my-org` +- **THEN** the profile node description reads `( - my-org)` + +#### Scenario: Empty alias omits the separator + +- **WHEN** the selected profile's account has no alias +- **THEN** the profile node description reads `()` with no trailing `-` + +### Requirement: Resources view reflects edits to the active view + +When the Resources view is showing a user-defined saved view (filter) and that view's definition is edited, the Resources view SHALL refresh to reflect the new definition without the user reselecting it. Saved-view focus selectors SHALL resolve their definition from current settings at selection/refresh time rather than from a snapshot captured when the tree was built. When the currently active saved view is removed, the Resources view SHALL revert to its no-focus placeholder. + +#### Scenario: Editing the active view updates the Resources view + +- **WHEN** a saved view is the active focus in the Resources view and the user edits that view's service/resource-type pairs +- **THEN** the Resources view refreshes to show the edited view's pairs without the user reselecting it + +#### Scenario: Removing the active view clears the Resources view + +- **WHEN** the active saved view is removed +- **THEN** the Resources view reverts to its placeholder prompting the user to select a focus + +### Requirement: Manual refresh of the Resources and Resource Details views + +The Resources and Resource Details views SHALL each provide a refresh action in the view's title bar. Invoking it SHALL re-fetch and re-render the view's current content — the Resources view against its active focus, and the Resource Details view against its currently selected resource — without requiring the user to re-select a focus or resource. Refreshing the Resources view SHALL **recompute** the active focus from its source rather than re-rendering a cached structure; for a LocalStack instance focus this means re-querying the metamodel API, so resources created since the focus was first selected appear. + +#### Scenario: Refreshing the Resources view re-fetches the active focus + +- **WHEN** the user clicks the refresh action in the Resources view title bar while a focus is active +- **THEN** the view re-queries the platform and re-renders the resources for that same focus + +#### Scenario: Refreshing a LocalStack instance focus picks up new resources + +- **WHEN** the active focus came from a LocalStack instance "All Resources" selector and a new resource has since been created in the emulator +- **THEN** clicking refresh re-queries the metamodel, recomputes the focus, and the new resource appears without the user re-selecting the focus selector + +#### Scenario: Refreshing the Resource Details view re-fetches the selected resource + +- **WHEN** the user clicks the refresh action in the Resource Details view title bar while a resource is selected +- **THEN** the view re-fetches and re-renders that resource's fields diff --git a/openspec/specs/service-catalog/spec.md b/openspec/specs/service-catalog/spec.md new file mode 100644 index 0000000..9e28882 --- /dev/null +++ b/openspec/specs/service-catalog/spec.md @@ -0,0 +1,75 @@ +# service-catalog Specification + +## Purpose +The static, coverage-derived service manifest (the single source of truth for which services exist), the requirement that every manifest service has a curated provider with no generic tier or runtime discovery, and the declarative provider format/engine used to author them. + +## Requirements +### Requirement: Static service manifest derived from published coverage + +The system SHALL maintain a static service manifest enumerating every service LocalStack publishes as supported, generated from LocalStack's coverage data (`localstack-docs/src/data/coverage/*.json`) and committed to the repository. Each manifest entry SHALL use the service's AWS service code as its id (e.g. `s3`, `secretsmanager`, `cognito-idp`, `logs`, `events`, `states`) and a display name. Every published service SHALL be included regardless of community/pro availability, and the system SHALL NOT store or display the availability distinction — because the browser also targets real AWS, all services are treated as fully available to everyone. The manifest SHALL be the single source of truth for which services exist, and SHALL be regenerated only on demand (when a developer notices it is out of date). The system SHALL NOT query a running emulator (e.g. `/_localstack/health`) or any discovery API (e.g. Cloud Control) to determine the supported set. + +#### Scenario: Manifest covers the full published service set + +- **WHEN** the manifest is loaded +- **THEN** it contains an entry for every service in LocalStack's published coverage data, keyed by the AWS service code, including services with no hand-written provider yet + +#### Scenario: Supported set is not discovered at runtime + +- **WHEN** the resource browser needs to know which services exist +- **THEN** it reads the static manifest and does not call `/_localstack/health`, Cloud Control, or any other runtime discovery endpoint + +### Requirement: Every supported service has a curated provider + +The system SHALL provide, for each manifest service, a curated provider that declares the service's resource types (each with singular/plural display names), lists the live resources for a profile/region/resource-type, and describes a single resource. There SHALL be no generic or fallback provider that serves arbitrary services without per-service curation. Provider resolution SHALL return the curated provider for a manifest service id. A manifest service that does not yet have a registered provider SHALL be absent from the browser rather than served generically. Completeness — every manifest service having a registered provider — is the definition of done and SHALL be enforced by an automated check. + +#### Scenario: Resolution returns the curated provider + +- **WHEN** a provider is resolved for a manifest service id that has a registered provider +- **THEN** that curated provider instance is returned + +#### Scenario: No generic fallback + +- **WHEN** a manifest service has no registered provider yet +- **THEN** the service is omitted from the resource browser, and no generic provider is used in its place + +#### Scenario: Completeness is enforced + +- **WHEN** the completeness check runs +- **THEN** it reports any manifest service id with no registered provider, and passes only when every manifest service has one + +### Requirement: Per-service detail fields + +Each curated provider SHALL define, per resource type, a fixed, ordered, typed subset of fields shown in the Resource Details view (field label, value path, and `FieldType`). This field set MAY be initially produced by a build-time generator that ranks the resource's API response members by importance, and SHALL be committed and hand-editable thereafter. At runtime the view SHALL render that selected subset — not the raw, unfiltered API response. + +#### Scenario: Detail fields are a selected, typed subset + +- **WHEN** a resource of a curated service is selected +- **THEN** the Resource Details view shows that resource type's defined field subset (labels, values, and types), not a generic dump of every response key + +#### Scenario: Field selection is authored, not computed at runtime + +- **WHEN** a provider's detail fields are determined +- **THEN** they come from the committed per-resource-type field spec (optionally generated by importance at build time and refined by hand), not from inspecting the response shape at runtime + +### Requirement: Declarative provider definitions + +The system SHALL support authoring curated providers as declarative definitions (data describing each resource type's list call and identifier, CloudFormation type mapping, and detail fields), executed by a shared engine that adapts them to the provider interface. The imperative `ServiceProvider` class SHALL remain available as an escape hatch for services that cannot be expressed declaratively. A declarative provider SHALL be functionally equivalent to an imperative one. + +#### Scenario: A declarative definition behaves as a provider + +- **WHEN** a service is authored as a declarative definition and resolved through the provider factory +- **THEN** it lists resource types, lists resources, and describes resources exactly as an imperative provider would + +#### Scenario: Escape hatch remains available + +- **WHEN** a service's behavior cannot be expressed in the declarative format +- **THEN** it MAY be implemented as an imperative `ServiceProvider` subclass and registered the same way + +### Requirement: Service-code mapping for emulator labels + +The system SHALL map service labels that appear in emulator data (metamodel labels, CloudFormation resource namespaces) to manifest service-code ids, case-insensitively with a documented override table for exceptions (e.g. `stepfunctions → states`, `StepFunctions → states`). Ids SHALL be the AWS service codes (e.g. `cognito-idp`). + +#### Scenario: Override mapping resolves exceptions + +- **WHEN** an emulator label does not equal a manifest id under a simple lowercase transform (e.g. Step Functions) +- **THEN** the documented override mapping resolves it to the correct manifest id diff --git a/package.json b/package.json index a3e5574..8ab36ce 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,87 @@ "type": "string", "default": "", "markdownDescription": "Location of LocalStack CLI." + }, + "localstack.cloudProfiles.regions": { + "type": "object", + "default": {}, + "markdownDescription": "User-added regions per AWS profile shown under Cloud Profiles. Managed via the profile row's \"Select Regions...\" / \"Remove Region\" actions.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "localstack.cloudProfiles.shown": { + "type": "array", + "markdownDescription": "AWS profile names shown under the Cloud Profiles section. Managed via the section's \"Select Profiles...\" action; does not modify `~/.aws/config`. When unset, only the `default` profile is shown (or the first discovered profile if none is named `default`); an empty list explicitly shows no profiles.", + "items": { + "type": "string" + } + }, + "localstack.cloudProfiles.views": { + "type": "object", + "default": {}, + "markdownDescription": "User-defined resource views per AWS profile. Managed via the view's \"Add View...\" / \"Edit View...\" / \"Remove View\" actions.", + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "object", + "properties": { + "service": { + "type": "string" + }, + "resourceType": { + "type": "string" + } + } + } + }, + "scope": { + "type": "object" + } + } + } + } + }, + "localstack.instanceViews": { + "type": "array", + "default": [], + "markdownDescription": "User-defined resource views for the running LocalStack instance. Managed via the instance's \"Add View...\" / \"Edit View...\" / \"Remove View\" actions. Stored separately from Cloud Profiles views.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "object", + "properties": { + "service": { + "type": "string" + }, + "resourceType": { + "type": "string" + } + } + } + }, + "scope": { + "type": "object" + } + } + } } } }, @@ -56,14 +137,36 @@ "title": "LocalStack", "icon": "resources/icons/localstack.svg" } + ], + "panel": [ + { + "id": "localstackPanel", + "title": "Resource Details", + "icon": "resources/icons/localstack.svg" + } ] }, "views": { "localstackActivityBar": [ { "id": "localstack.instances", - "name": "LocalStack", - "icon": "resources/icons/localstack.svg" + "name": "Explore", + "icon": "resources/icons/localstack.svg", + "size": 1 + }, + { + "id": "localstack.resources", + "name": "Resources", + "icon": "resources/icons/localstack.svg", + "size": 1 + } + ], + "localstackPanel": [ + { + "id": "localstack.resourceDetails", + "name": "Resource Details", + "icon": "resources/icons/localstack.svg", + "type": "webview" } ] }, @@ -114,8 +217,215 @@ "command": "localstack.openAppInspector", "title": "Open App Inspector", "category": "LocalStack" + }, + { + "command": "localstack.addRegion", + "title": "Select Regions...", + "category": "LocalStack", + "icon": "$(settings-gear)" + }, + { + "command": "localstack.removeRegion", + "title": "Remove Region", + "category": "LocalStack", + "icon": "$(trash)" + }, + { + "command": "localstack.manageProfiles", + "title": "Select Profiles...", + "category": "LocalStack", + "icon": "$(settings-gear)" + }, + { + "command": "localstack.addProfileView", + "title": "Add View...", + "category": "LocalStack", + "icon": "$(add)" + }, + { + "command": "localstack.editProfileView", + "title": "Edit View...", + "category": "LocalStack", + "icon": "$(settings-gear)" + }, + { + "command": "localstack.removeProfileView", + "title": "Remove View", + "category": "LocalStack", + "icon": "$(trash)" + }, + { + "command": "localstack.addInstanceView", + "title": "Add View...", + "category": "LocalStack", + "icon": "$(add)" + }, + { + "command": "localstack.editInstanceView", + "title": "Edit View...", + "category": "LocalStack", + "icon": "$(settings-gear)" + }, + { + "command": "localstack.removeInstanceView", + "title": "Remove View", + "category": "LocalStack", + "icon": "$(trash)" + }, + { + "command": "localstack.refreshResources", + "title": "Refresh Resources", + "category": "LocalStack", + "icon": "$(refresh)" + }, + { + "command": "localstack.refreshResourceDetails", + "title": "Refresh Resource Details", + "category": "LocalStack", + "icon": "$(refresh)" } ], + "menus": { + "view/title": [ + { + "command": "localstack.refreshResources", + "when": "view == localstack.resources", + "group": "navigation" + }, + { + "command": "localstack.refreshResourceDetails", + "when": "view == localstack.resourceDetails", + "group": "navigation" + } + ], + "view/item/context": [ + { + "command": "localstack.manageProfiles", + "when": "view == localstack.instances && viewItem == localstackSection:profiles" + }, + { + "command": "localstack.manageProfiles", + "when": "view == localstack.instances && viewItem == localstackSection:profiles", + "group": "inline" + }, + { + "command": "localstack.addRegion", + "when": "view == localstack.instances && viewItem == localstackProfile" + }, + { + "command": "localstack.addRegion", + "when": "view == localstack.instances && viewItem == localstackProfile", + "group": "inline" + }, + { + "command": "localstack.addProfileView", + "when": "view == localstack.instances && (viewItem == localstackDefaultRegion || viewItem == localstackUserRegion)", + "group": "1_region@1" + }, + { + "command": "localstack.addProfileView", + "when": "view == localstack.instances && (viewItem == localstackDefaultRegion || viewItem == localstackUserRegion)", + "group": "inline@1" + }, + { + "command": "localstack.removeRegion", + "when": "view == localstack.instances && viewItem == localstackUserRegion", + "group": "1_region@2" + }, + { + "command": "localstack.removeRegion", + "when": "view == localstack.instances && viewItem == localstackUserRegion", + "group": "inline@2" + }, + { + "command": "localstack.editProfileView", + "when": "view == localstack.instances && viewItem == localstackProfileView", + "group": "1_modify@1" + }, + { + "command": "localstack.editProfileView", + "when": "view == localstack.instances && viewItem == localstackProfileView", + "group": "inline@1" + }, + { + "command": "localstack.removeProfileView", + "when": "view == localstack.instances && viewItem == localstackProfileView", + "group": "1_modify@2" + }, + { + "command": "localstack.removeProfileView", + "when": "view == localstack.instances && viewItem == localstackProfileView", + "group": "inline@2" + }, + { + "command": "localstack.addInstanceView", + "when": "view == localstack.instances && viewItem == localstackInstance" + }, + { + "command": "localstack.addInstanceView", + "when": "view == localstack.instances && viewItem == localstackInstance", + "group": "inline" + }, + { + "command": "localstack.editInstanceView", + "when": "view == localstack.instances && viewItem == localstackInstanceView", + "group": "1_modify@1" + }, + { + "command": "localstack.editInstanceView", + "when": "view == localstack.instances && viewItem == localstackInstanceView", + "group": "inline@1" + }, + { + "command": "localstack.removeInstanceView", + "when": "view == localstack.instances && viewItem == localstackInstanceView", + "group": "1_modify@2" + }, + { + "command": "localstack.removeInstanceView", + "when": "view == localstack.instances && viewItem == localstackInstanceView", + "group": "inline@2" + } + ], + "commandPalette": [ + { + "command": "localstack.addRegion", + "when": "false" + }, + { + "command": "localstack.removeRegion", + "when": "false" + }, + { + "command": "localstack.manageProfiles", + "when": "false" + }, + { + "command": "localstack.addProfileView", + "when": "false" + }, + { + "command": "localstack.editProfileView", + "when": "false" + }, + { + "command": "localstack.removeProfileView", + "when": "false" + }, + { + "command": "localstack.addInstanceView", + "when": "false" + }, + { + "command": "localstack.editInstanceView", + "when": "false" + }, + { + "command": "localstack.removeInstanceView", + "when": "false" + } + ] + }, "icons": { "localstack-logo": { "description": "LocalStack logo", @@ -126,12 +436,10 @@ } } }, - "packageManager": "pnpm@11.0.9+sha512.34ce82e6780233cf9cad8685029a8f81d2e06196c5a9bad98879f7424940c6817c4e4524fb7d38b8553ceed48b9758b8ebaf1abd3600c232c4c8cf7366086f38", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "scripts": { "vscode:prepublish": "pnpm run package", - "compile": "npm-run-all -p compile:extension compile:appinspector-webview", - "compile:appinspector-webview": "vite build", - "compile:extension": "node --env-file=.env.local --env-file-if-exists=.env build/extension.mjs", + "compile": "node --env-file=.env.local --env-file-if-exists=.env build/extension.mjs && vite build", "compile:font": "node build/icon-font.mjs", "dev": "npm-run-all -p dev:extension dev:localstack-web-mock-server dev:appinspector-webview", "dev:appinspector-webview": "vite build --watch --no-clear-screen", @@ -146,6 +454,25 @@ "test": "vscode-test" }, "devDependencies": { + "@aws-sdk/client-account": "^3.901.0", + "@aws-sdk/client-api-gateway": "^3.901.0", + "@aws-sdk/client-cloudformation": "^3.901.0", + "@aws-sdk/client-cloudwatch-logs": "^3.901.0", + "@aws-sdk/client-cognito-identity-provider": "^3.901.0", + "@aws-sdk/client-dynamodb": "^3.901.0", + "@aws-sdk/client-ecr": "^3.901.0", + "@aws-sdk/client-eventbridge": "^3.901.0", + "@aws-sdk/client-iam": "^3.901.0", + "@aws-sdk/client-kinesis": "^3.901.0", + "@aws-sdk/client-kms": "^3.901.0", + "@aws-sdk/client-lambda": "^3.901.0", + "@aws-sdk/client-s3": "^3.901.0", + "@aws-sdk/client-secrets-manager": "^3.901.0", + "@aws-sdk/client-sfn": "^3.901.0", + "@aws-sdk/client-sns": "^3.901.0", + "@aws-sdk/client-sqs": "^3.901.0", + "@aws-sdk/client-ssm": "^3.901.0", + "@aws-sdk/client-sts": "^3.901.0", "@biomejs/biome": "^2.4.13", "@eslint/compat": "^2.0.5", "@eslint/js": "^9.39.4", @@ -167,6 +494,7 @@ "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "fs-extra": "^11.3.4", + "js-ini": "^1.6.0", "ms": "^2.1.3", "npm-run-all": "^4.1.5", "p-min-delay": "^4.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a908d67..dd26922 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,21 +15,78 @@ importers: .: devDependencies: + '@aws-sdk/client-account': + specifier: ^3.901.0 + version: 3.1071.0 + '@aws-sdk/client-api-gateway': + specifier: ^3.901.0 + version: 3.1071.0 + '@aws-sdk/client-cloudformation': + specifier: ^3.901.0 + version: 3.1071.0 + '@aws-sdk/client-cloudwatch-logs': + specifier: ^3.901.0 + version: 3.1071.0 + '@aws-sdk/client-cognito-identity-provider': + specifier: ^3.901.0 + version: 3.1071.0 + '@aws-sdk/client-dynamodb': + specifier: ^3.901.0 + version: 3.1071.0 + '@aws-sdk/client-ecr': + specifier: ^3.901.0 + version: 3.1071.0 + '@aws-sdk/client-eventbridge': + specifier: ^3.901.0 + version: 3.1071.0 + '@aws-sdk/client-iam': + specifier: ^3.901.0 + version: 3.1070.0 + '@aws-sdk/client-kinesis': + specifier: ^3.901.0 + version: 3.1070.0 + '@aws-sdk/client-kms': + specifier: ^3.901.0 + version: 3.1070.0 + '@aws-sdk/client-lambda': + specifier: ^3.901.0 + version: 3.1070.0 + '@aws-sdk/client-s3': + specifier: ^3.901.0 + version: 3.1071.0 + '@aws-sdk/client-secrets-manager': + specifier: ^3.901.0 + version: 3.1070.0 + '@aws-sdk/client-sfn': + specifier: ^3.901.0 + version: 3.1070.0 + '@aws-sdk/client-sns': + specifier: ^3.901.0 + version: 3.1070.0 + '@aws-sdk/client-sqs': + specifier: ^3.901.0 + version: 3.1070.0 + '@aws-sdk/client-ssm': + specifier: ^3.901.0 + version: 3.1070.0 + '@aws-sdk/client-sts': + specifier: ^3.901.0 + version: 3.1070.0 '@biomejs/biome': specifier: ^2.4.13 - version: 2.4.13 + version: 2.5.0 '@eslint/compat': specifier: ^2.0.5 - version: 2.0.5(eslint@9.39.4) + version: 2.1.0(eslint@9.39.4) '@eslint/js': specifier: ^9.39.4 version: 9.39.4 '@localstack/appinspector-ui': specifier: ^1.0.135 - version: 1.0.135(7e1f8c1137e0c97a778d73cefcced92d) + version: 1.0.146(09e585e523679aaf310539e99633db80) '@localstack/integrations': specifier: ^1.2.2 - version: 1.2.2(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + version: 1.2.2(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 @@ -41,19 +98,19 @@ importers: version: 2.1.0 '@types/node': specifier: 20.x - version: 20.19.0 + version: 20.19.43 '@types/react': specifier: ^17.0.89 - version: 17.0.91 + version: 17.0.93 '@types/react-dom': specifier: ^17.0.26 - version: 17.0.26(@types/react@17.0.91) + version: 17.0.26(@types/react@17.0.93) '@types/vscode': specifier: ^1.83.0 - version: 1.103.0 + version: 1.125.0 '@vitejs/plugin-react': specifier: ^5.1.0 - version: 5.2.0(vite@7.3.2(@types/node@20.19.0)) + version: 5.2.0(vite@7.3.5(@types/node@20.19.43)) '@vscode/test-cli': specifier: ^0.0.12 version: 0.0.12 @@ -62,22 +119,25 @@ importers: version: 2.5.2 '@vscode/vsce': specifier: ^3.9.1 - version: 3.9.1 + version: 3.9.2 chokidar: specifier: ^4.0.3 version: 4.0.3 esbuild: specifier: ^0.28.0 - version: 0.28.0 + version: 0.28.1 eslint: specifier: ^9.39.4 version: 9.39.4 eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4) + version: 2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4) fs-extra: specifier: ^11.3.4 - version: 11.3.4 + version: 11.3.5 + js-ini: + specifier: ^1.6.0 + version: 1.6.0 ms: specifier: ^2.1.3 version: 2.1.3 @@ -95,25 +155,220 @@ importers: version: 17.0.2(react@17.0.2) react-router-dom: specifier: ^6.30.1 - version: 6.30.3(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + version: 6.30.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2) typescript: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: specifier: ^8.59.2 - version: 8.59.2(eslint@9.39.4)(typescript@5.9.3) + version: 8.61.1(eslint@9.39.4)(typescript@5.9.3) uuid: specifier: ^14.0.0 version: 14.0.0 vite: specifier: ^7.2.1 - version: 7.3.2(@types/node@20.19.0) + version: 7.3.5(@types/node@20.19.43) zod: specifier: ^4.4.3 version: 4.4.3 packages: + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/checksums@3.1000.7': + resolution: {integrity: sha512-qh0fG/RtrFztst4+vn1HZehAvAhr5Jlq/WMP7e5KvvfF16oNVBc9CDNVdxdm19vzOY2x0qiDMFCRjhxQAusGWQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-account@3.1071.0': + resolution: {integrity: sha512-zfSB/Rzreyn20IEa5mEgHlLEi18Vc6u2uqM0ThDrMaU6CZf2g2GRQKk23bkF4xVyqQ4XS7V+wMXbCQMUhi/y8A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-api-gateway@3.1071.0': + resolution: {integrity: sha512-AhKwHq9TtYU2iZ3stpT/jO3lVrR5ubcuKWtQal3y/en1UGH4SFMCIj2ezhG20jTNRlFnXI74/IYgwjw4LglBvg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-cloudformation@3.1071.0': + resolution: {integrity: sha512-SibX2MXJzcH0SCpTCvYVWiMBI06kRHD3ZpHKT0CP445MVobdcSM+6ivBY7PdV9M71x4Y1CcuHhcl/wF2q3fHmg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-cloudwatch-logs@3.1071.0': + resolution: {integrity: sha512-8l4evPw4V6N9rKKCGBP39Dc3bzaPf0ytqgCkVHTeGtaJg8ih6XCdEIuKFBTYTQpFUbPu010Tt+rEQ8GuJnSKoQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-cognito-identity-provider@3.1071.0': + resolution: {integrity: sha512-EgsfMw0CEWCQ2pkWXiu6acx4BmsM/CPYq4dUcq4ai1JLJpS14R2KB/TUARkf+nHhHPDUZQgMjbJ5BNhUFzesqA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-dynamodb@3.1071.0': + resolution: {integrity: sha512-LgocgtjPBHurOdx2FUcXaN9K+0C/r7bXkQV56vCi/ZHNfkTf6kQe+WhqrYxXp8QE+lrnjxrDzsxWW7wR0I4yLA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-ecr@3.1071.0': + resolution: {integrity: sha512-N9t2N+lBOrmO6V0Z4Tx+pjs8or8WTwxFawa7pc7EwBvpdlwapE71rq8O9+GZvw9w1k1pwZSsTwFeGBgCopNiGQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-eventbridge@3.1071.0': + resolution: {integrity: sha512-zRfJzr5UAoOJFuks6XYVDWgqsgr3Tm+lcmaMzi83s/um02mzC+jc8PkEEqdGHalzhY9qUeVEm6iP9mWha1doZg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-iam@3.1070.0': + resolution: {integrity: sha512-IC/S11y/e7bxwpgvieFQx4nMn/fsOjNj85n03PQfrQb52X/wC2/Ha25ZD3LOnP1DIWrfDcy3dltKkznbQz1Mfw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-kinesis@3.1070.0': + resolution: {integrity: sha512-PoYjA2UX9upzmL37vl6qQ8SyoKT4tT8UWYEMmKv34Dw172eqnL7qs+WZY0JPDOLA2n1AMmzjYcHdd8G2oyP7jQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-kms@3.1070.0': + resolution: {integrity: sha512-ELJH11w1IU0V5kqQ/eg2iu4HqaXOJQ/3nC8I7y93+mZW39osINlOHFKgYQNizwZA5KoSxSgVYmLPnnF/i0oKmw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-lambda@3.1070.0': + resolution: {integrity: sha512-v9xBUFCtLnhrdf/sWZETvGggTDDMF0dLTQ/D9AmHBXSOjJ9BZ8O3V2Mr0htfu5A58dK8Rkxcw5f7YMuFui35mw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1071.0': + resolution: {integrity: sha512-BqsqkaU/FztbQnq5Aw0BK6/weQgwnC3n2w19M7CjEjRHscr5dZU8+ihi7PIY6UMW9RkJrzUUEmaoHQrIVScFYQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-secrets-manager@3.1070.0': + resolution: {integrity: sha512-5t6TmKsESxrrd6iD1tRXaiQxusjboZa846pLZZUwvt4M2xy3FsHUaQoZ50ZPnSr5DNWJubbrHuB+jKn4LGO1eQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-sfn@3.1070.0': + resolution: {integrity: sha512-TvqXRE5dPyF+oM1u4o6WzEUA0jO2JOGotWeXq9+iyrDhgFGwxgjtZ+qa+vZ+yxrQK/S6UXcCbD8tWnWbDLuVAA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-sns@3.1070.0': + resolution: {integrity: sha512-JtOVpLGuaUhJbewPW08Q3xMTrXhJ+G2nQV4wQkItHm80/WNX0dCEYZSwKek/CfhMHImgzWJkB8s7yPvMxX50LA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-sqs@3.1070.0': + resolution: {integrity: sha512-3eOY4zpl3ePnsg5eQv7pfLjYqhxXBSwkIga/1ELDO9sdnPGmt7asGAUQNrHg/R9K7JQzK9nDQD38pnmGAyzWLw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-ssm@3.1070.0': + resolution: {integrity: sha512-jSw0/1/PrGurRknme/lpVBE49vXtIAwib04eEBlmDKF2XcDG78/e2wsVi51b9G3z8U0GGI4v1dsojx9XabpJqg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-sts@3.1070.0': + resolution: {integrity: sha512-uX3eD0OxYbaHqbN8aaSzZTrNJUctfCus2NLMVHUSONrzlJdA/w4o5ZAfJhgnTjlfdfRMv1lZd2Wnqm8hlzmwGA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.22': + resolution: {integrity: sha512-YofH63shc6YRdXjz80BJkpJW+Bkn0Cuu2dn4Rv7s9G2Idt58tgtzQEWxrR2xVljlVfIBeUjPuULnSVYLke3sUQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.48': + resolution: {integrity: sha512-h6FEC95fbexUd6zxm4PdgS82bTcI2PRtUb2ZwMipb/Xr8bPwtf0G8rBo2jp7NA24Mbx2JA8/WingiYpA9RCCyw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.50': + resolution: {integrity: sha512-lJO3OLpjvz5m/RSBQmsG/CEUGsvCy5ruxKwPQaOCqxqCMuyYT2BZwQUTDZVVwqQ9LrZKuK24JSa6r31hL/tvkg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.55': + resolution: {integrity: sha512-TBoF4buBGYhXjdZAryayY2TrkQj2B2KfE/msG4V53XCt+w0EhEwM2JRjx8p2grJ2C6gtH5++SAwEvGMRdi0yyw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.54': + resolution: {integrity: sha512-hBWI3wZTdTGiuMfmPts6AWbAjFfRniOQnqx68tc2cQvRKWawFbN9wkLOVPWM1FAOyowZU73mC6Fi+rHSHNyLFw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.57': + resolution: {integrity: sha512-u6dClpzNdWf1HGWz4wwhdXi1wiOofCLniM9S4BQQGlLAN9TW7VB+ld5V533GdKrYMaFeBGFqKnj0JCYvynLqwQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.48': + resolution: {integrity: sha512-w6VZwojPt12WnEkAUy6Nu4K6sWCbBmR7QX390b0nE6vRvkXbrYr9Lq9VySGkfjiMjpUA87op+J4EgvRmtWIDoQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.54': + resolution: {integrity: sha512-23uZpIpF2SIFDCa1fcWa202tK4gGeyvX6GIIAjiB8WBsvsVRBMnJ/7dCxHzxf7eZT7GToJg837LDIBnZsl/VUg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.54': + resolution: {integrity: sha512-0Iv5QttS6wcATlodYKgvQj6B9Db51rx7NU9fqu0PoLeS4BIgdYMc/QK4smwLwpm5RFrs02V/eLyEFp3FklvlNQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/dynamodb-codec@3.973.22': + resolution: {integrity: sha512-QBs7/nWHsPA0wTEqhU5iPgaDjeZzQHhmwJr6Q0f56rW3Fk8ExJQgXFzEg/l48iDwJVUIMLjPqhw7dNP3cShUxA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/endpoint-cache@3.972.8': + resolution: {integrity: sha512-bBmkG0Dnhfq0/T4Z0PpUr7HkncBVaWvvCbvafeaUM+yC9wa8GGjLJmonq0QL17REB9WivgGeYgWQ5A80Uw5UnQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-endpoint-discovery@3.972.19': + resolution: {integrity: sha512-FMgyzUq3Jh+ONRYxryBRNdBd+FUX8PwRl07ccQknNdoms6KCeAEusCkl6whqpDrPQ6OH0ddeSifKyqYSs2DLIw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.32': + resolution: {integrity: sha512-KhuzFMzUbb3oEj43CdPDbEJ/RG/RkErkmXk3J/LE8OPFNvkCn8PYPMpjOLgzAzvxBacsSyytdWf+R50q0alJ4w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-api-gateway@3.972.18': + resolution: {integrity: sha512-3xZO1L3f+OshQ+ChcyCQtwZ2eeK7V4xqAxZ3cBDVgyEd8HTnIol9t4UNB5YoCaXOtYWduCtyFDBzKT0tSEAjGQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.53': + resolution: {integrity: sha512-keWp6Z5cEIJzPwoCf/WRm0ceAeephPDDivhRsK/xXs2ZYXyypJ2/DL9G1IR0bz/s+iZC0EgzmFV4r7rlvLlxQQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-sqs@3.972.31': + resolution: {integrity: sha512-56ifsBmK9bLn5EE/t6c0nmjOB1BO8cJDLkA1VOlsN1GR85ROqnaCwVDspqcwsLaBDgPlwyYNedoDIoT3t6Ho1A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.22': + resolution: {integrity: sha512-4IwtcYSxEIVw5hcp8ogq0CMbFNZFw7jJUetpfFUhFFeqsa1K8j2Ihg2hnxLyOp3stMZnXda6VzOmPi1AFZQXcg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.35': + resolution: {integrity: sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1071.0': + resolution: {integrity: sha512-4LDW2Qob6LoLFuqYSYZq2AyTE9koSE9+i+n5UZcm10GpmQOK0zRD9L4uYlzItiTKksIWgC/qMFChAi3RvKYtMg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.13': + resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.30': + resolution: {integrity: sha512-StElZPEoBquWwNqw1AcfpzEyZqJvFxouG+mpDNYlcH6ZOrqd2CuIryv+8LV8gNHZUOyKyJF3Dq9vxaXEmDR9TQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@azu/format-text@1.0.2': resolution: {integrity: sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==} @@ -124,24 +379,24 @@ packages: resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} - '@azure/core-auth@1.10.0': - resolution: {integrity: sha512-88Djs5vBvGbHQHf5ZZcaoNHo6Y8BKZkt3cw2iuJIQzLEgH4Ox6Tm4hjFhbqOxyYsgIG/eJbFEHpxRIfEEWv5Ow==} + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} engines: {node: '>=20.0.0'} - '@azure/core-client@1.10.0': - resolution: {integrity: sha512-O4aP3CLFNodg8eTHXECaH3B3CjicfzkxVtnrfLkOq0XNP7TIECGfHpK/C6vADZkWP75wzmdBnsIA8ksuJMk18g==} + '@azure/core-client@1.10.2': + resolution: {integrity: sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==} engines: {node: '>=20.0.0'} - '@azure/core-rest-pipeline@1.22.0': - resolution: {integrity: sha512-OKHmb3/Kpm06HypvB3g6Q3zJuvyXcpxDpCS1PnU8OV6AJgSFaee/covXBcPbWc6XDDxtEPlbi3EMQ6nUiPaQtw==} + '@azure/core-rest-pipeline@1.24.0': + resolution: {integrity: sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg==} engines: {node: '>=20.0.0'} - '@azure/core-tracing@1.3.0': - resolution: {integrity: sha512-+XvmZLLWPe67WXNZo9Oc9CrPj/Tm8QnHR92fFAFdnbzwNdCH1h+7UdpaQgRSBsMY+oW1kHXNUZQLdZ1gHX3ROw==} + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} engines: {node: '>=20.0.0'} - '@azure/core-util@1.13.0': - resolution: {integrity: sha512-o0psW8QWQ58fq3i24Q1K2XfS/jYTxr7O1HRcyUE9bV9NttLU+kYOH82Ixj8DGlMTOWgxm1Sss2QAfKK5UkSPxw==} + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} engines: {node: '>=20.0.0'} '@azure/identity@4.13.1': @@ -152,162 +407,162 @@ packages: resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} engines: {node: '>=20.0.0'} - '@azure/msal-browser@5.9.0': - resolution: {integrity: sha512-CzE+4PefDSJWj26zU7G1bKchlGRRHMBFreG4tAlGuzyI8hAPiYGobaJvZBgZBf6L63iphX7VH+ityL8VgEQz9Q==} + '@azure/msal-browser@5.14.0': + resolution: {integrity: sha512-Dfl7hPZe9/JJwRhFFXHq2z1oHYBuGubmff3kWXOsd1AGgyXlqjNYAWuN/1JL/ZrcZBs8TKMjGSil6Rcc7E8VPQ==} engines: {node: '>=0.8.0'} - '@azure/msal-common@16.5.2': - resolution: {integrity: sha512-GkDEL6TYo3HgT3UuqakdgE9PZfc1hMki6+Hwgy1uddb/EauvAKfu85vVhuofRSo22D1xTnWt8Ucwfg4vSCVwvA==} + '@azure/msal-common@16.9.0': + resolution: {integrity: sha512-1MWGjqgUCRAYgLmVFZKp7fs3Rg1TFvIMgywY8ze2olNVvLlJoRThuoziWSDJuwwyJI5L4rnLb9Tyt5D9GvSLPw==} engines: {node: '>=0.8.0'} - '@azure/msal-node@5.1.5': - resolution: {integrity: sha512-ObTeMoNPmq19X3z40et9Xvs4ZoWVeJg43PZMRLG5iwVL+2nCtAerG3YTDItqPp1CfXNwmCXBbg8jn1DOx65c3g==} + '@azure/msal-node@5.2.5': + resolution: {integrity: sha512-RUuewWk9JvWJS5Yiy8/74Lm1rQAWlrU/qg/Bgtk1jIauVRtnb9XKwS5Xg0J+Whwjesq9EVrBIFgQEP8vHxgezA==} engines: {node: '>=20'} - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@biomejs/biome@2.4.13': - resolution: {integrity: sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA==} + '@biomejs/biome@2.5.0': + resolution: {integrity: sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.4.13': - resolution: {integrity: sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg==} + '@biomejs/cli-darwin-arm64@2.5.0': + resolution: {integrity: sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.4.13': - resolution: {integrity: sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg==} + '@biomejs/cli-darwin-x64@2.5.0': + resolution: {integrity: sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.4.13': - resolution: {integrity: sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg==} + '@biomejs/cli-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.4.13': - resolution: {integrity: sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==} + '@biomejs/cli-linux-arm64@2.5.0': + resolution: {integrity: sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.4.13': - resolution: {integrity: sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ==} + '@biomejs/cli-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.4.13': - resolution: {integrity: sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ==} + '@biomejs/cli-linux-x64@2.5.0': + resolution: {integrity: sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.4.13': - resolution: {integrity: sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==} + '@biomejs/cli-win32-arm64@2.5.0': + resolution: {integrity: sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.4.13': - resolution: {integrity: sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ==} + '@biomejs/cli-win32-x64@2.5.0': + resolution: {integrity: sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -379,8 +634,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -391,8 +646,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.28.0': - resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -403,8 +658,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.28.0': - resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -415,8 +670,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.28.0': - resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -427,8 +682,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.28.0': - resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -439,8 +694,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.28.0': - resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -451,8 +706,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.28.0': - resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -463,8 +718,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.0': - resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -475,8 +730,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.28.0': - resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -487,8 +742,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.28.0': - resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -499,8 +754,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.28.0': - resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -511,8 +766,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.28.0': - resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -523,8 +778,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.28.0': - resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -535,8 +790,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.28.0': - resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -547,8 +802,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.28.0': - resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -559,8 +814,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.28.0': - resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -571,8 +826,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.28.0': - resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -583,8 +838,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.28.0': - resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -595,8 +850,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.0': - resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -607,8 +862,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.28.0': - resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -619,8 +874,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.0': - resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -631,8 +886,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.28.0': - resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -643,8 +898,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.28.0': - resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -655,8 +910,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.28.0': - resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -667,8 +922,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.28.0': - resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -679,8 +934,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.28.0': - resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -695,8 +950,8 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@2.0.5': - resolution: {integrity: sha512-IbHDbHJfkVNv6xjlET8AIVo/K1NQt7YT4Rp6ok/clyBGcpRx1l6gv0Rq3vBvYfPJIZt6ODf66Zq08FJNDpnzgg==} + '@eslint/compat@2.1.0': + resolution: {integrity: sha512-LgaSCymEpw7tF53xvDw9SNsraPb1IBHxpdABIOM0hW8UAlP8znrjYtuxfR58FSJ3L9BhwD+FaPRFQpZq84Nh6g==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: ^8.40 || 9 || 10 @@ -736,22 +991,22 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.1': - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} @@ -760,8 +1015,8 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} '@jridgewell/gen-mapping@0.3.13': @@ -780,8 +1035,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@localstack/appinspector-ui@1.0.135': - resolution: {integrity: sha512-yAvVNVhAu6SGTNvV6j/r1XlhOWnyVSV288TYxvO8ULIOjsAtL+vCmjVFbYmhy+T3gSoBlovXgbR709KLwlTVQw==} + '@localstack/appinspector-ui@1.0.146': + resolution: {integrity: sha512-Zo2MHn3jD2uVAcAivJFF9c1cGCeL4LjyAx0avW87KgoFheU85HjieBSHijSIlMz9OIv3HFD1S24xEg5tpNZfdg==} peerDependencies: '@dagrejs/dagre': ^1 '@emotion/react': ^11 @@ -899,6 +1154,9 @@ packages: '@types/react': optional: true + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -918,148 +1176,148 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@remix-run/router@1.23.2': - resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} + '@remix-run/router@1.23.3': + resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==} engines: {node: '>=14.0.0'} '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + '@rollup/rollup-android-arm-eabi@4.62.0': + resolution: {integrity: sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + '@rollup/rollup-android-arm64@4.62.0': + resolution: {integrity: sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + '@rollup/rollup-darwin-arm64@4.62.0': + resolution: {integrity: sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + '@rollup/rollup-darwin-x64@4.62.0': + resolution: {integrity: sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + '@rollup/rollup-freebsd-arm64@4.62.0': + resolution: {integrity: sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + '@rollup/rollup-freebsd-x64@4.62.0': + resolution: {integrity: sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + '@rollup/rollup-linux-arm-gnueabihf@4.62.0': + resolution: {integrity: sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + '@rollup/rollup-linux-arm-musleabihf@4.62.0': + resolution: {integrity: sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + '@rollup/rollup-linux-arm64-gnu@4.62.0': + resolution: {integrity: sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + '@rollup/rollup-linux-arm64-musl@4.62.0': + resolution: {integrity: sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + '@rollup/rollup-linux-loong64-gnu@4.62.0': + resolution: {integrity: sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + '@rollup/rollup-linux-loong64-musl@4.62.0': + resolution: {integrity: sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + '@rollup/rollup-linux-ppc64-gnu@4.62.0': + resolution: {integrity: sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + '@rollup/rollup-linux-ppc64-musl@4.62.0': + resolution: {integrity: sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + '@rollup/rollup-linux-riscv64-gnu@4.62.0': + resolution: {integrity: sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + '@rollup/rollup-linux-riscv64-musl@4.62.0': + resolution: {integrity: sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + '@rollup/rollup-linux-s390x-gnu@4.62.0': + resolution: {integrity: sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + '@rollup/rollup-linux-x64-gnu@4.62.0': + resolution: {integrity: sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + '@rollup/rollup-linux-x64-musl@4.62.0': + resolution: {integrity: sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + '@rollup/rollup-openbsd-x64@4.62.0': + resolution: {integrity: sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + '@rollup/rollup-openharmony-arm64@4.62.0': + resolution: {integrity: sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + '@rollup/rollup-win32-arm64-msvc@4.62.0': + resolution: {integrity: sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + '@rollup/rollup-win32-ia32-msvc@4.62.0': + resolution: {integrity: sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + '@rollup/rollup-win32-x64-gnu@4.62.0': + resolution: {integrity: sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + '@rollup/rollup-win32-x64-msvc@4.62.0': + resolution: {integrity: sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==} cpu: [x64] os: [win32] @@ -1115,20 +1373,56 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} - '@textlint/ast-node-types@15.2.2': - resolution: {integrity: sha512-9ByYNzWV8tpz6BFaRzeRzIov8dkbSZu9q7IWqEIfmRuLWb2qbI/5gTvKcoWT1HYs4XM7IZ8TKSXcuPvMb6eorA==} + '@smithy/core@3.25.1': + resolution: {integrity: sha512-zpDbpXBCBsxfLtG2GEUyfgvHvSFrw5CwDZSNzL0v52gx/c3oPlPbm+7W7num8xs6vyiUBn+bvYPHcQDOXZynCQ==} + engines: {node: '>=18.0.0'} - '@textlint/linter-formatter@15.2.2': - resolution: {integrity: sha512-oMVaMJ3exFvXhCj3AqmCbLaeYrTNLqaJnLJMIlmnRM3/kZdxvku4OYdaDzgtlI194cVxamOY5AbHBBVnY79kEg==} + '@smithy/credential-provider-imds@4.4.1': + resolution: {integrity: sha512-TSAF5NHgxEsllbErYWbK8aLnl5L601NGc5VYJlSPsKnf3YlkhdoBN+geGcaU00oiw2OK3QO5LA3QNXiiWhCidQ==} + engines: {node: '>=18.0.0'} - '@textlint/module-interop@15.2.2': - resolution: {integrity: sha512-2rmNcWrcqhuR84Iio1WRzlc4tEoOMHd6T7urjtKNNefpTt1owrTJ9WuOe60yD3FrTW0J/R0ux5wxUbP/eaeFOA==} + '@smithy/fetch-http-handler@5.5.1': + resolution: {integrity: sha512-96JrD1q71anokymx9Iblb+zKmNQYNstlV/25A9ZYIJ2A0rp1r7/GZAIm0bDWSmVvz3DpNOCZuabzsiL+w0UHhw==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.8.1': + resolution: {integrity: sha512-emtXvoky671puri18ETf64AFIQUGIEA093F2drXpBgB0OGnBLjcwNR3CA2mYu62IAqNsS56xa5lnTxAgPq7cjw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.5.1': + resolution: {integrity: sha512-X9rVls3En0z3NtrmguTmpRM0/NqtWUxBjal6fcAkwtsub+gOdLZ6kD+V7xhUgFMGdG14bHbZ7M5QjaRI1+DatQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.15.0': + resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} - '@textlint/resolver@15.2.2': - resolution: {integrity: sha512-4hGWjmHt0y+5NAkoYZ8FvEkj8Mez9TqfbTm3BPjoV32cIfEixl2poTOgapn1rfm73905GSO3P1jiWjmgvii13Q==} + '@textlint/ast-node-types@15.7.1': + resolution: {integrity: sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==} - '@textlint/types@15.2.2': - resolution: {integrity: sha512-X2BHGAR3yXJsCAjwYEDBIk9qUDWcH4pW61ISfmtejau+tVqKtnbbvEZnMTb6mWgKU1BvTmftd5DmB1XVDUtY3g==} + '@textlint/linter-formatter@15.7.1': + resolution: {integrity: sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==} + + '@textlint/module-interop@15.7.1': + resolution: {integrity: sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==} + + '@textlint/resolver@15.7.1': + resolution: {integrity: sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==} + + '@textlint/types@15.7.1': + resolution: {integrity: sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==} '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1160,8 +1454,8 @@ packages: '@types/d3-zoom@3.0.8': resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} @@ -1184,8 +1478,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@20.19.0': - resolution: {integrity: sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==} + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1206,8 +1500,8 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@17.0.91': - resolution: {integrity: sha512-xauZca6qMeCU3Moy0KxCM9jtf1vyk6qRYK39Ryf3afUqwgNUjRIGoDdS9BcGWgAMGSg1hvP4XcmlYrM66PtqeA==} + '@types/react@17.0.93': + resolution: {integrity: sha512-KM4Ty/ZTLZupiYxZVAlP+InNJS3De6uBMdq0ePa6/04+eG9Y7ftnWfst1xTLQ5rwAhgHwQ4momt/O4KepdGBTw==} '@types/sarif@2.1.7': resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} @@ -1215,70 +1509,70 @@ packages: '@types/scheduler@0.16.8': resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - '@types/vscode@1.103.0': - resolution: {integrity: sha512-o4hanZAQdNfsKecexq9L3eHICd0AAvdbLk6hA60UzGXbGH/q8b/9xv2RgR7vV3ZcHuyKVq7b37IGd/+gM4Tu+Q==} + '@types/vscode@1.125.0': + resolution: {integrity: sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==} - '@typescript-eslint/eslint-plugin@8.59.2': - resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} + '@typescript-eslint/eslint-plugin@8.61.1': + resolution: {integrity: sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.59.2 + '@typescript-eslint/parser': ^8.61.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.59.2': - resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} + '@typescript-eslint/parser@8.61.1': + resolution: {integrity: sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.59.2': - resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} + '@typescript-eslint/project-service@8.61.1': + resolution: {integrity: sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.59.2': - resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} + '@typescript-eslint/scope-manager@8.61.1': + resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.59.2': - resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} + '@typescript-eslint/tsconfig-utils@8.61.1': + resolution: {integrity: sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.59.2': - resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==} + '@typescript-eslint/type-utils@8.61.1': + resolution: {integrity: sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.59.2': - resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + '@typescript-eslint/types@8.61.1': + resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.59.2': - resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} + '@typescript-eslint/typescript-estree@8.61.1': + resolution: {integrity: sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.59.2': - resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} + '@typescript-eslint/utils@8.61.1': + resolution: {integrity: sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.59.2': - resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} + '@typescript-eslint/visitor-keys@8.61.1': + resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typespec/ts-http-runtime@0.3.0': - resolution: {integrity: sha512-sOx1PKSuFwnIl7z4RN0Ls7N9AQawmR9r66eI5rFCzLDIs8HTIYrIpH9QjYWoX0lkgGrkLxXhi4QnK7MizPRrIg==} + '@typespec/ts-http-runtime@0.3.6': + resolution: {integrity: sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==} engines: {node: '>=20.0.0'} '@vitejs/plugin-react@5.2.0': @@ -1296,90 +1590,97 @@ packages: resolution: {integrity: sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==} engines: {node: '>=16'} - '@vscode/vsce-sign-alpine-arm64@2.0.5': - resolution: {integrity: sha512-XVmnF40APwRPXSLYA28Ye+qWxB25KhSVpF2eZVtVOs6g7fkpOxsVnpRU1Bz2xG4ySI79IRuapDJoAQFkoOgfdQ==} + '@vscode/vsce-sign-alpine-arm64@2.0.6': + resolution: {integrity: sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==} cpu: [arm64] os: [alpine] - '@vscode/vsce-sign-alpine-x64@2.0.5': - resolution: {integrity: sha512-JuxY3xcquRsOezKq6PEHwCgd1rh1GnhyH6urVEWUzWn1c1PC4EOoyffMD+zLZtFuZF5qR1I0+cqDRNKyPvpK7Q==} + '@vscode/vsce-sign-alpine-x64@2.0.6': + resolution: {integrity: sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==} cpu: [x64] os: [alpine] - '@vscode/vsce-sign-darwin-arm64@2.0.5': - resolution: {integrity: sha512-z2Q62bk0ptADFz8a0vtPvnm6vxpyP3hIEYMU+i1AWz263Pj8Mc38cm/4sjzxu+LIsAfhe9HzvYNS49lV+KsatQ==} + '@vscode/vsce-sign-darwin-arm64@2.0.6': + resolution: {integrity: sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==} cpu: [arm64] os: [darwin] - '@vscode/vsce-sign-darwin-x64@2.0.5': - resolution: {integrity: sha512-ma9JDC7FJ16SuPXlLKkvOD2qLsmW/cKfqK4zzM2iJE1PbckF3BlR08lYqHV89gmuoTpYB55+z8Y5Fz4wEJBVDA==} + '@vscode/vsce-sign-darwin-x64@2.0.6': + resolution: {integrity: sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==} cpu: [x64] os: [darwin] - '@vscode/vsce-sign-linux-arm64@2.0.5': - resolution: {integrity: sha512-Hr1o0veBymg9SmkCqYnfaiUnes5YK6k/lKFA5MhNmiEN5fNqxyPUCdRZMFs3Ajtx2OFW4q3KuYVRwGA7jdLo7Q==} + '@vscode/vsce-sign-linux-arm64@2.0.6': + resolution: {integrity: sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==} cpu: [arm64] os: [linux] - '@vscode/vsce-sign-linux-arm@2.0.5': - resolution: {integrity: sha512-cdCwtLGmvC1QVrkIsyzv01+o9eR+wodMJUZ9Ak3owhcGxPRB53/WvrDHAFYA6i8Oy232nuen1YqWeEohqBuSzA==} + '@vscode/vsce-sign-linux-arm@2.0.6': + resolution: {integrity: sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==} cpu: [arm] os: [linux] - '@vscode/vsce-sign-linux-x64@2.0.5': - resolution: {integrity: sha512-XLT0gfGMcxk6CMRLDkgqEPTyG8Oa0OFe1tPv2RVbphSOjFWJwZgK3TYWx39i/7gqpDHlax0AP6cgMygNJrA6zg==} + '@vscode/vsce-sign-linux-x64@2.0.6': + resolution: {integrity: sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==} cpu: [x64] os: [linux] - '@vscode/vsce-sign-win32-arm64@2.0.5': - resolution: {integrity: sha512-hco8eaoTcvtmuPhavyCZhrk5QIcLiyAUhEso87ApAWDllG7djIrWiOCtqn48k4pHz+L8oCQlE0nwNHfcYcxOPw==} + '@vscode/vsce-sign-win32-arm64@2.0.6': + resolution: {integrity: sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==} cpu: [arm64] os: [win32] - '@vscode/vsce-sign-win32-x64@2.0.5': - resolution: {integrity: sha512-1ixKFGM2FwM+6kQS2ojfY3aAelICxjiCzeg4nTHpkeU1Tfs4RC+lVLrgq5NwcBC7ZLr6UfY3Ct3D6suPeOf7BQ==} + '@vscode/vsce-sign-win32-x64@2.0.6': + resolution: {integrity: sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==} cpu: [x64] os: [win32] - '@vscode/vsce-sign@2.0.6': - resolution: {integrity: sha512-j9Ashk+uOWCDHYDxgGsqzKq5FXW9b9MW7QqOIYZ8IYpneJclWTBeHZz2DJCSKQgo+JAqNcaRRE1hzIx0dswqAw==} + '@vscode/vsce-sign@2.0.9': + resolution: {integrity: sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==} - '@vscode/vsce@3.9.1': - resolution: {integrity: sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA==} + '@vscode/vsce@3.9.2': + resolution: {integrity: sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==} engines: {node: '>= 20'} hasBin: true - '@xyflow/react@12.10.2': - resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==} + '@xyflow/react@12.11.0': + resolution: {integrity: sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==} peerDependencies: + '@types/react': '>=17' + '@types/react-dom': '>=17' react: '>=17' react-dom: '>=17' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@xyflow/system@0.0.76': - resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==} + '@xyflow/system@0.0.77': + resolution: {integrity: sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} hasBin: true - agent-base@7.1.3: - resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - ansi-escapes@7.0.0: - resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} ansi-regex@5.0.1: @@ -1406,8 +1707,8 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + anynum@1.0.0: + resolution: {integrity: sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1468,8 +1769,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.13: - resolution: {integrity: sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==} + baseline-browser-mapping@2.10.38: + resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==} engines: {node: '>=6.0.0'} hasBin: true @@ -1490,14 +1791,17 @@ packages: boundary@2.0.0: resolution: {integrity: sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==} - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} - brace-expansion@2.1.0: - resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -1539,8 +1843,8 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} engines: {node: '>= 0.4'} call-bound@1.0.4: @@ -1555,8 +1859,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001784: - resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==} + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -1573,8 +1877,8 @@ packages: cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - cheerio@1.1.2: - resolution: {integrity: sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==} + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} engines: {node: '>=20.18.1'} chokidar@3.6.0: @@ -1751,12 +2055,12 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - default-browser-id@5.0.0: - resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} - default-browser@5.2.1: - resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} engines: {node: '>=18'} define-data-property@1.1.4: @@ -1775,8 +2079,8 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} diff@8.0.4: @@ -1817,11 +2121,11 @@ packages: resolution: {integrity: sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==} engines: {ecmascript: '>= es5', node: '>=4'} - electron-to-chromium@1.5.330: - resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==} + electron-to-chromium@1.5.375: + resolution: {integrity: sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==} - emoji-regex@10.5.0: - resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1835,8 +2139,8 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + enhanced-resolve@5.24.0: + resolution: {integrity: sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -1847,18 +2151,23 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -1869,8 +2178,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: @@ -1881,8 +2190,8 @@ packages: resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} engines: {node: '>= 0.4'} - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + es-to-primitive@1.3.1: + resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} engines: {node: '>= 0.4'} esbuild@0.27.7: @@ -1890,8 +2199,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.28.0: - resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -1907,11 +2216,11 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + eslint-module-utils@2.13.0: + resolution: {integrity: sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -1971,13 +2280,8 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -2012,8 +2316,15 @@ packages: fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} + hasBin: true + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -2058,15 +2369,15 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@11.3.4: - resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + fs-extra@11.3.5: + resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} engines: {node: '>=14.14'} fsevents@2.3.3: @@ -2077,13 +2388,17 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} engines: {node: '>= 0.4'} functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2092,8 +2407,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.3.1: - resolution: {integrity: sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} get-intrinsic@1.3.0: @@ -2124,11 +2439,9 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@11.1.0: - resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} - engines: {node: 20 || >=22} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -2176,12 +2489,8 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} he@1.2.0: @@ -2205,8 +2514,8 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - htmlparser2@10.0.0: - resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} @@ -2245,8 +2554,8 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - index-to-position@1.1.0: - resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} engines: {node: '>=18'} inherits@2.0.4: @@ -2286,10 +2595,6 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} @@ -2307,6 +2612,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2319,8 +2628,8 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} is-glob@4.0.3: @@ -2411,8 +2720,8 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} - is-wsl@3.1.0: - resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} isarray@1.0.0: @@ -2443,19 +2752,14 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.1.1: - resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} - engines: {node: 20 || >=22} + js-ini@1.6.0: + resolution: {integrity: sha512-9Vx+NVkMRNY4i7pLLIGdXIQbP1iwB3fP2IG1zMCgeUUvR1ODmTKa33eR/J9FHoorNsfoJr9/2SVqeSKLwPD9Nw==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} - hasBin: true - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true jsesc@3.1.0: @@ -2493,8 +2797,8 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} @@ -2553,8 +2857,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + linkify-it@5.0.1: + resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} @@ -2609,8 +2913,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.1.0: - resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -2624,8 +2928,8 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - markdown-it@14.1.1: - resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + markdown-it@14.2.0: + resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} hasBin: true math-intrinsics@1.1.0: @@ -2682,15 +2986,18 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mocha@11.7.5: - resolution: {integrity: sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==} + mnemonist@0.38.3: + resolution: {integrity: sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==} + + mocha@11.7.6: + resolution: {integrity: sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true @@ -2700,8 +3007,8 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2714,20 +3021,25 @@ packages: nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - node-abi@3.75.0: - resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} + node-abi@3.92.0: + resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} engines: {node: '>=10'} node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} - node-releases@2.0.36: - resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} - node-sarif-builder@3.2.0: - resolution: {integrity: sha512-kVIOdynrF2CRodHZeP/97Rh1syTUHBNiw17hUCIVhlhEsWlfJm19MuO56s4MdKbr22xWx6mzMnNAgXzVlIYM9Q==} + node-releases@2.0.47: + resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} engines: {node: '>=18'} + node-sarif-builder@3.4.0: + resolution: {integrity: sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==} + engines: {node: '>=20'} + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -2763,6 +3075,10 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + object.fromentries@2.0.8: resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} engines: {node: '>= 0.4'} @@ -2775,6 +3091,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obliterator@1.6.1: + resolution: {integrity: sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2806,8 +3125,8 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-map@7.0.3: - resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} + p-map@7.0.4: + resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} engines: {node: '>=18'} p-min-delay@4.2.0: @@ -2852,6 +3171,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-key@2.0.1: resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} engines: {node: '>=4'} @@ -2867,9 +3190,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.0: - resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} - engines: {node: 20 || >=22} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} @@ -2917,8 +3240,8 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.12: - resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} prebuild-install@7.1.3: @@ -2937,8 +3260,8 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - pump@3.0.3: - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} @@ -2948,15 +3271,15 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.15.0: - resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - rc-config-loader@4.1.3: - resolution: {integrity: sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==} + rc-config-loader@4.1.4: + resolution: {integrity: sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==} rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} @@ -2970,22 +3293,22 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@19.2.4: - resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==} + react-is@19.2.7: + resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==} react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} - react-router-dom@6.30.3: - resolution: {integrity: sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==} + react-router-dom@6.30.4: + resolution: {integrity: sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' - react-router@6.30.3: - resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==} + react-router@6.30.4: + resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' @@ -3047,13 +3370,13 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} hasBin: true - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} engines: {node: '>= 0.4'} hasBin: true @@ -3065,25 +3388,28 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + rollup@4.62.0: + resolution: {integrity: sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - run-applescript@7.0.0: - resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -3095,8 +3421,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sax@1.4.1: - resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} scheduler@0.20.2: resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} @@ -3114,13 +3441,13 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} hasBin: true - serialize-javascript@7.0.5: - resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} + serialize-javascript@7.0.6: + resolution: {integrity: sha512-ATTK5Q4gFVg0YDp1my2vqygyvhcklD/UV5GIlYHooGTn/NogJqIzpetkD6E5kmuVULqz/S9inUL25XcAgDRJQg==} engines: {node: '>=20.0.0'} set-function-length@1.2.2: @@ -3154,12 +3481,12 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} side-channel-map@1.0.1: @@ -3170,8 +3497,8 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} signal-exit@4.1.0: @@ -3209,11 +3536,8 @@ packages: spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - spdx-license-ids@3.0.21: - resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} @@ -3239,12 +3563,12 @@ packages: resolution: {integrity: sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==} engines: {node: '>= 0.4'} - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} engines: {node: '>= 0.4'} - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} engines: {node: '>= 0.4'} string.prototype.trimstart@1.0.8: @@ -3254,12 +3578,15 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} strip-bom@3.0.0: @@ -3274,6 +3601,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@2.4.0: + resolution: {integrity: sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==} + structured-source@4.0.0: resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==} @@ -3308,8 +3638,8 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} tar-fs@2.1.4: @@ -3337,12 +3667,12 @@ packages: tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tmp@0.2.5: - resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} to-regex-range@5.0.1: @@ -3388,15 +3718,15 @@ packages: resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} engines: {node: '>= 0.4'} - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} - typescript-eslint@8.59.2: - resolution: {integrity: sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==} + typescript-eslint@8.61.1: + resolution: {integrity: sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -3420,8 +3750,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@7.24.5: - resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} unicorn-magic@0.1.0: @@ -3471,8 +3801,8 @@ packages: resolution: {integrity: sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==} engines: {node: '>=4'} - vite@7.3.2: - resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} + vite@7.3.5: + resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3532,8 +3862,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} engines: {node: '>= 0.4'} which@1.3.1: @@ -3567,6 +3897,10 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} @@ -3601,8 +3935,8 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yauzl@3.3.0: - resolution: {integrity: sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==} + yauzl@3.4.0: + resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} engines: {node: '>=12'} yazl@2.5.1: @@ -3636,6 +3970,510 @@ packages: snapshots: + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/checksums@3.1000.7': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-account@3.1071.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-api-gateway@3.1071.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/middleware-sdk-api-gateway': 3.972.18 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-cloudformation@3.1071.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-cloudwatch-logs@3.1071.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-cognito-identity-provider@3.1071.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-dynamodb@3.1071.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/dynamodb-codec': 3.973.22 + '@aws-sdk/middleware-endpoint-discovery': 3.972.19 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-ecr@3.1071.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-eventbridge@3.1071.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-iam@3.1070.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-kinesis@3.1070.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-kms@3.1070.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-lambda@3.1070.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1071.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/middleware-flexible-checksums': 3.974.32 + '@aws-sdk/middleware-sdk-s3': 3.972.53 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-secrets-manager@3.1070.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-sfn@3.1070.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-sns@3.1070.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-sqs@3.1070.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/middleware-sdk-sqs': 3.972.31 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-ssm@3.1070.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-sts@3.1070.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.22': + dependencies: + '@aws-sdk/types': 3.973.13 + '@aws-sdk/xml-builder': 3.972.30 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.25.1 + '@smithy/signature-v4': 5.5.1 + '@smithy/types': 4.15.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.48': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.50': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-env': 3.972.48 + '@aws-sdk/credential-provider-http': 3.972.50 + '@aws-sdk/credential-provider-login': 3.972.54 + '@aws-sdk/credential-provider-process': 3.972.48 + '@aws-sdk/credential-provider-sso': 3.972.54 + '@aws-sdk/credential-provider-web-identity': 3.972.54 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/credential-provider-imds': 4.4.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.54': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.57': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.48 + '@aws-sdk/credential-provider-http': 3.972.50 + '@aws-sdk/credential-provider-ini': 3.972.55 + '@aws-sdk/credential-provider-process': 3.972.48 + '@aws-sdk/credential-provider-sso': 3.972.54 + '@aws-sdk/credential-provider-web-identity': 3.972.54 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/credential-provider-imds': 4.4.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.48': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.54': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/token-providers': 3.1071.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.54': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/dynamodb-codec@3.973.22': + dependencies: + '@aws-sdk/core': 3.974.22 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/endpoint-cache@3.972.8': + dependencies: + mnemonist: 0.38.3 + tslib: 2.8.1 + + '@aws-sdk/middleware-endpoint-discovery@3.972.19': + dependencies: + '@aws-sdk/endpoint-cache': 3.972.8 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.32': + dependencies: + '@aws-sdk/checksums': 3.1000.7 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-api-gateway@3.972.18': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.53': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-sqs@3.972.31': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.22': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.35': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/signature-v4': 5.5.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1071.0': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.13': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.8': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.30': + dependencies: + '@smithy/types': 4.15.0 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@azu/format-text@1.0.2': {} '@azu/style-format@1.0.1': @@ -3646,46 +4484,46 @@ snapshots: dependencies: tslib: 2.8.1 - '@azure/core-auth@1.10.0': + '@azure/core-auth@1.10.1': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-util': 1.13.0 + '@azure/core-util': 1.13.1 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-client@1.10.0': + '@azure/core-client@1.10.2': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.0 - '@azure/core-rest-pipeline': 1.22.0 - '@azure/core-tracing': 1.3.0 - '@azure/core-util': 1.13.0 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.24.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 '@azure/logger': 1.3.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-rest-pipeline@1.22.0': + '@azure/core-rest-pipeline@1.24.0': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.0 - '@azure/core-tracing': 1.3.0 - '@azure/core-util': 1.13.0 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 '@azure/logger': 1.3.0 - '@typespec/ts-http-runtime': 0.3.0 + '@typespec/ts-http-runtime': 0.3.6 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-tracing@1.3.0': + '@azure/core-tracing@1.3.1': dependencies: tslib: 2.8.1 - '@azure/core-util@1.13.0': + '@azure/core-util@1.13.1': dependencies: '@azure/abort-controller': 2.1.2 - '@typespec/ts-http-runtime': 0.3.0 + '@typespec/ts-http-runtime': 0.3.6 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -3693,14 +4531,14 @@ snapshots: '@azure/identity@4.13.1': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.0 - '@azure/core-client': 1.10.0 - '@azure/core-rest-pipeline': 1.22.0 - '@azure/core-tracing': 1.3.0 - '@azure/core-util': 1.13.0 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.2 + '@azure/core-rest-pipeline': 1.24.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 '@azure/logger': 1.3.0 - '@azure/msal-browser': 5.9.0 - '@azure/msal-node': 5.1.5 + '@azure/msal-browser': 5.14.0 + '@azure/msal-node': 5.2.5 open: 10.2.0 tslib: 2.8.1 transitivePeerDependencies: @@ -3708,41 +4546,41 @@ snapshots: '@azure/logger@1.3.0': dependencies: - '@typespec/ts-http-runtime': 0.3.0 + '@typespec/ts-http-runtime': 0.3.6 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/msal-browser@5.9.0': + '@azure/msal-browser@5.14.0': dependencies: - '@azure/msal-common': 16.5.2 + '@azure/msal-common': 16.9.0 - '@azure/msal-common@16.5.2': {} + '@azure/msal-common@16.9.0': {} - '@azure/msal-node@5.1.5': + '@azure/msal-node@5.2.5': dependencies: - '@azure/msal-common': 16.5.2 + '@azure/msal-common': 16.9.0 jsonwebtoken: 9.0.3 - '@babel/code-frame@7.29.0': + '@babel/code-frame@7.29.7': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.0': {} + '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.0': + '@babel/core@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@8.1.1) @@ -3752,127 +4590,127 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.1': + '@babel/generator@7.29.7': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.28.6': + '@babel/helper-compilation-targets@7.29.7': dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-globals@7.28.0': {} + '@babel/helper-globals@7.29.7': {} - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-validator-option@7.29.7': {} - '@babel/helpers@7.29.2': + '@babel/helpers@7.29.7': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 - '@babel/parser@7.29.2': + '@babel/parser@7.29.7': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.7': {} - '@babel/template@7.28.6': + '@babel/template@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 - '@babel/traverse@7.29.0': + '@babel/traverse@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': + '@babel/types@7.29.7': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 '@bcoe/v8-coverage@1.0.2': {} - '@biomejs/biome@2.4.13': + '@biomejs/biome@2.5.0': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.4.13 - '@biomejs/cli-darwin-x64': 2.4.13 - '@biomejs/cli-linux-arm64': 2.4.13 - '@biomejs/cli-linux-arm64-musl': 2.4.13 - '@biomejs/cli-linux-x64': 2.4.13 - '@biomejs/cli-linux-x64-musl': 2.4.13 - '@biomejs/cli-win32-arm64': 2.4.13 - '@biomejs/cli-win32-x64': 2.4.13 - - '@biomejs/cli-darwin-arm64@2.4.13': + '@biomejs/cli-darwin-arm64': 2.5.0 + '@biomejs/cli-darwin-x64': 2.5.0 + '@biomejs/cli-linux-arm64': 2.5.0 + '@biomejs/cli-linux-arm64-musl': 2.5.0 + '@biomejs/cli-linux-x64': 2.5.0 + '@biomejs/cli-linux-x64-musl': 2.5.0 + '@biomejs/cli-win32-arm64': 2.5.0 + '@biomejs/cli-win32-x64': 2.5.0 + + '@biomejs/cli-darwin-arm64@2.5.0': optional: true - '@biomejs/cli-darwin-x64@2.4.13': + '@biomejs/cli-darwin-x64@2.5.0': optional: true - '@biomejs/cli-linux-arm64-musl@2.4.13': + '@biomejs/cli-linux-arm64-musl@2.5.0': optional: true - '@biomejs/cli-linux-arm64@2.4.13': + '@biomejs/cli-linux-arm64@2.5.0': optional: true - '@biomejs/cli-linux-x64-musl@2.4.13': + '@biomejs/cli-linux-x64-musl@2.5.0': optional: true - '@biomejs/cli-linux-x64@2.4.13': + '@biomejs/cli-linux-x64@2.5.0': optional: true - '@biomejs/cli-win32-arm64@2.4.13': + '@biomejs/cli-win32-arm64@2.5.0': optional: true - '@biomejs/cli-win32-x64@2.4.13': + '@biomejs/cli-win32-x64@2.5.0': optional: true '@dagrejs/dagre@1.1.8': @@ -3883,8 +4721,8 @@ snapshots: '@emotion/babel-plugin@11.13.5': dependencies: - '@babel/helper-module-imports': 7.28.6 - '@babel/runtime': 7.29.2 + '@babel/helper-module-imports': 7.29.7 + '@babel/runtime': 7.29.7 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -3913,9 +4751,9 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2)': + '@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 @@ -3925,7 +4763,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 17.0.2 optionalDependencies: - '@types/react': 17.0.91 + '@types/react': 17.0.93 transitivePeerDependencies: - supports-color @@ -3939,18 +4777,18 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.4.0 - '@emotion/react': 11.14.0(@types/react@17.0.91)(react@17.0.2) + '@emotion/react': 11.14.0(@types/react@17.0.93)(react@17.0.2) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@17.0.2) '@emotion/utils': 1.4.2 react: 17.0.2 optionalDependencies: - '@types/react': 17.0.91 + '@types/react': 17.0.93 transitivePeerDependencies: - supports-color @@ -3967,157 +4805,157 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/aix-ppc64@0.28.0': + '@esbuild/aix-ppc64@0.28.1': optional: true '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm64@0.28.0': + '@esbuild/android-arm64@0.28.1': optional: true '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-arm@0.28.0': + '@esbuild/android-arm@0.28.1': optional: true '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/android-x64@0.28.0': + '@esbuild/android-x64@0.28.1': optional: true '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.28.0': + '@esbuild/darwin-arm64@0.28.1': optional: true '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/darwin-x64@0.28.0': + '@esbuild/darwin-x64@0.28.1': optional: true '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.28.0': + '@esbuild/freebsd-arm64@0.28.1': optional: true '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.28.0': + '@esbuild/freebsd-x64@0.28.1': optional: true '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm64@0.28.0': + '@esbuild/linux-arm64@0.28.1': optional: true '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-arm@0.28.0': + '@esbuild/linux-arm@0.28.1': optional: true '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-ia32@0.28.0': + '@esbuild/linux-ia32@0.28.1': optional: true '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-loong64@0.28.0': + '@esbuild/linux-loong64@0.28.1': optional: true '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-mips64el@0.28.0': + '@esbuild/linux-mips64el@0.28.1': optional: true '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-ppc64@0.28.0': + '@esbuild/linux-ppc64@0.28.1': optional: true '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.28.0': + '@esbuild/linux-riscv64@0.28.1': optional: true '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-s390x@0.28.0': + '@esbuild/linux-s390x@0.28.1': optional: true '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/linux-x64@0.28.0': + '@esbuild/linux-x64@0.28.1': optional: true '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.28.0': + '@esbuild/netbsd-arm64@0.28.1': optional: true '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.28.0': + '@esbuild/netbsd-x64@0.28.1': optional: true '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.28.0': + '@esbuild/openbsd-arm64@0.28.1': optional: true '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.28.0': + '@esbuild/openbsd-x64@0.28.1': optional: true '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.28.0': + '@esbuild/openharmony-arm64@0.28.1': optional: true '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/sunos-x64@0.28.0': + '@esbuild/sunos-x64@0.28.1': optional: true '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-arm64@0.28.0': + '@esbuild/win32-arm64@0.28.1': optional: true '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-ia32@0.28.0': + '@esbuild/win32-ia32@0.28.1': optional: true '@esbuild/win32-x64@0.27.7': optional: true - '@esbuild/win32-x64@0.28.0': + '@esbuild/win32-x64@0.28.1': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': @@ -4127,7 +4965,7 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@2.0.5(eslint@9.39.4)': + '@eslint/compat@2.1.0(eslint@9.39.4)': dependencies: '@eslint/core': 1.2.1 optionalDependencies: @@ -4155,13 +4993,13 @@ snapshots: '@eslint/eslintrc@3.3.5': dependencies: - ajv: 6.14.0 + ajv: 6.15.0 debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -4176,16 +5014,19 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@humanfs/core@0.19.1': {} + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 - '@humanfs/node@0.16.6': + '@humanfs/node@0.16.8': dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 - '@humanwhocodes/module-importer@1.0.1': {} + '@humanfs/types@0.15.0': {} - '@humanwhocodes/retry@0.3.1': {} + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.4.3': {} @@ -4193,12 +5034,12 @@ snapshots: dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@istanbuljs/schema@0.1.3': {} + '@istanbuljs/schema@0.1.6': {} '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -4219,25 +5060,25 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@localstack/appinspector-ui@1.0.135(7e1f8c1137e0c97a778d73cefcced92d)': + '@localstack/appinspector-ui@1.0.146(09e585e523679aaf310539e99633db80)': dependencies: '@dagrejs/dagre': 1.1.8 - '@emotion/react': 11.14.0(@types/react@17.0.91)(react@17.0.2) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2) - '@localstack/integrations': 1.2.2(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@mui/icons-material': 5.18.0(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@types/react@17.0.91)(react@17.0.2) - '@mui/material': 5.18.0(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@mui/styles': 5.18.0(@types/react@17.0.91)(react@17.0.2) - '@xyflow/react': 12.10.2(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@emotion/react': 11.14.0(@types/react@17.0.93)(react@17.0.2) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2) + '@localstack/integrations': 1.2.2(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@mui/icons-material': 5.18.0(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@types/react@17.0.93)(react@17.0.2) + '@mui/material': 5.18.0(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@mui/styles': 5.18.0(@types/react@17.0.93)(react@17.0.2) + '@xyflow/react': 12.11.0(@types/react-dom@17.0.26(@types/react@17.0.93))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - react-router-dom: 6.30.3(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - zustand: 4.5.7(@types/react@17.0.91)(react@17.0.2) + react-router-dom: 6.30.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + zustand: 4.5.7(@types/react@17.0.93)(react@17.0.2) - '@localstack/integrations@1.2.2(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@localstack/integrations@1.2.2(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: - '@mui/material': 5.18.0(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@mui/styles': 5.18.0(@types/react@17.0.91)(react@17.0.2) + '@mui/material': 5.18.0(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@mui/styles': 5.18.0(@types/react@17.0.93)(react@17.0.2) react: 17.0.2 transitivePeerDependencies: - '@emotion/react' @@ -4247,63 +5088,63 @@ snapshots: '@mui/core-downloads-tracker@5.18.0': {} - '@mui/icons-material@5.18.0(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@types/react@17.0.91)(react@17.0.2)': + '@mui/icons-material@5.18.0(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@types/react@17.0.93)(react@17.0.2)': dependencies: - '@babel/runtime': 7.29.2 - '@mui/material': 5.18.0(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@babel/runtime': 7.29.7 + '@mui/material': 5.18.0(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) react: 17.0.2 optionalDependencies: - '@types/react': 17.0.91 + '@types/react': 17.0.93 - '@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@mui/core-downloads-tracker': 5.18.0 - '@mui/system': 5.18.0(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2) - '@mui/types': 7.2.24(@types/react@17.0.91) - '@mui/utils': 5.17.1(@types/react@17.0.91)(react@17.0.2) + '@mui/system': 5.18.0(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2) + '@mui/types': 7.2.24(@types/react@17.0.93) + '@mui/utils': 5.17.1(@types/react@17.0.93)(react@17.0.2) '@popperjs/core': 2.11.8 - '@types/react-transition-group': 4.4.12(@types/react@17.0.91) + '@types/react-transition-group': 4.4.12(@types/react@17.0.93) clsx: 2.1.1 csstype: 3.2.3 prop-types: 15.8.1 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - react-is: 19.2.4 + react-is: 19.2.7 react-transition-group: 4.4.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@17.0.91)(react@17.0.2) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2) - '@types/react': 17.0.91 + '@emotion/react': 11.14.0(@types/react@17.0.93)(react@17.0.2) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2) + '@types/react': 17.0.93 - '@mui/private-theming@5.17.1(@types/react@17.0.91)(react@17.0.2)': + '@mui/private-theming@5.17.1(@types/react@17.0.93)(react@17.0.2)': dependencies: - '@babel/runtime': 7.29.2 - '@mui/utils': 5.17.1(@types/react@17.0.91)(react@17.0.2) + '@babel/runtime': 7.29.7 + '@mui/utils': 5.17.1(@types/react@17.0.93)(react@17.0.2) prop-types: 15.8.1 react: 17.0.2 optionalDependencies: - '@types/react': 17.0.91 + '@types/react': 17.0.93 - '@mui/styled-engine@5.18.0(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(react@17.0.2)': + '@mui/styled-engine@5.18.0(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(react@17.0.2)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 csstype: 3.2.3 prop-types: 15.8.1 react: 17.0.2 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@17.0.91)(react@17.0.2) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2) + '@emotion/react': 11.14.0(@types/react@17.0.93)(react@17.0.2) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2) - '@mui/styles@5.18.0(@types/react@17.0.91)(react@17.0.2)': + '@mui/styles@5.18.0(@types/react@17.0.93)(react@17.0.2)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@emotion/hash': 0.9.2 - '@mui/private-theming': 5.17.1(@types/react@17.0.91)(react@17.0.2) - '@mui/types': 7.2.24(@types/react@17.0.91) - '@mui/utils': 5.17.1(@types/react@17.0.91)(react@17.0.2) + '@mui/private-theming': 5.17.1(@types/react@17.0.93)(react@17.0.2) + '@mui/types': 7.2.24(@types/react@17.0.93) + '@mui/utils': 5.17.1(@types/react@17.0.93)(react@17.0.2) clsx: 2.1.1 csstype: 3.2.3 hoist-non-react-statics: 3.3.2 @@ -4318,39 +5159,41 @@ snapshots: prop-types: 15.8.1 react: 17.0.2 optionalDependencies: - '@types/react': 17.0.91 + '@types/react': 17.0.93 - '@mui/system@5.18.0(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2)': + '@mui/system@5.18.0(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2)': dependencies: - '@babel/runtime': 7.29.2 - '@mui/private-theming': 5.17.1(@types/react@17.0.91)(react@17.0.2) - '@mui/styled-engine': 5.18.0(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2))(react@17.0.2) - '@mui/types': 7.2.24(@types/react@17.0.91) - '@mui/utils': 5.17.1(@types/react@17.0.91)(react@17.0.2) + '@babel/runtime': 7.29.7 + '@mui/private-theming': 5.17.1(@types/react@17.0.93)(react@17.0.2) + '@mui/styled-engine': 5.18.0(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2))(react@17.0.2) + '@mui/types': 7.2.24(@types/react@17.0.93) + '@mui/utils': 5.17.1(@types/react@17.0.93)(react@17.0.2) clsx: 2.1.1 csstype: 3.2.3 prop-types: 15.8.1 react: 17.0.2 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@17.0.91)(react@17.0.2) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@17.0.91)(react@17.0.2))(@types/react@17.0.91)(react@17.0.2) - '@types/react': 17.0.91 + '@emotion/react': 11.14.0(@types/react@17.0.93)(react@17.0.2) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@17.0.93)(react@17.0.2))(@types/react@17.0.93)(react@17.0.2) + '@types/react': 17.0.93 - '@mui/types@7.2.24(@types/react@17.0.91)': + '@mui/types@7.2.24(@types/react@17.0.93)': optionalDependencies: - '@types/react': 17.0.91 + '@types/react': 17.0.93 - '@mui/utils@5.17.1(@types/react@17.0.91)(react@17.0.2)': + '@mui/utils@5.17.1(@types/react@17.0.93)(react@17.0.2)': dependencies: - '@babel/runtime': 7.29.2 - '@mui/types': 7.2.24(@types/react@17.0.91) + '@babel/runtime': 7.29.7 + '@mui/types': 7.2.24(@types/react@17.0.93) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 react: 17.0.2 - react-is: 19.2.4 + react-is: 19.2.7 optionalDependencies: - '@types/react': 17.0.91 + '@types/react': 17.0.93 + + '@nodable/entities@2.2.0': {} '@nodelib/fs.scandir@2.1.5': dependencies: @@ -4362,90 +5205,90 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + fastq: 1.20.1 '@pkgjs/parseargs@0.11.0': optional: true '@popperjs/core@2.11.8': {} - '@remix-run/router@1.23.2': {} + '@remix-run/router@1.23.3': {} '@rolldown/pluginutils@1.0.0-rc.3': {} - '@rollup/rollup-android-arm-eabi@4.60.1': + '@rollup/rollup-android-arm-eabi@4.62.0': optional: true - '@rollup/rollup-android-arm64@4.60.1': + '@rollup/rollup-android-arm64@4.62.0': optional: true - '@rollup/rollup-darwin-arm64@4.60.1': + '@rollup/rollup-darwin-arm64@4.62.0': optional: true - '@rollup/rollup-darwin-x64@4.60.1': + '@rollup/rollup-darwin-x64@4.62.0': optional: true - '@rollup/rollup-freebsd-arm64@4.60.1': + '@rollup/rollup-freebsd-arm64@4.62.0': optional: true - '@rollup/rollup-freebsd-x64@4.60.1': + '@rollup/rollup-freebsd-x64@4.62.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + '@rollup/rollup-linux-arm-gnueabihf@4.62.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.1': + '@rollup/rollup-linux-arm-musleabihf@4.62.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.1': + '@rollup/rollup-linux-arm64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.1': + '@rollup/rollup-linux-arm64-musl@4.62.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.1': + '@rollup/rollup-linux-loong64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.1': + '@rollup/rollup-linux-loong64-musl@4.62.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.1': + '@rollup/rollup-linux-ppc64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.1': + '@rollup/rollup-linux-ppc64-musl@4.62.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.1': + '@rollup/rollup-linux-riscv64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.1': + '@rollup/rollup-linux-riscv64-musl@4.62.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.1': + '@rollup/rollup-linux-s390x-gnu@4.62.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.1': + '@rollup/rollup-linux-x64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-x64-musl@4.60.1': + '@rollup/rollup-linux-x64-musl@4.62.0': optional: true - '@rollup/rollup-openbsd-x64@4.60.1': + '@rollup/rollup-openbsd-x64@4.62.0': optional: true - '@rollup/rollup-openharmony-arm64@4.60.1': + '@rollup/rollup-openharmony-arm64@4.62.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.1': + '@rollup/rollup-win32-arm64-msvc@4.62.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.1': + '@rollup/rollup-win32-ia32-msvc@4.62.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.1': + '@rollup/rollup-win32-x64-gnu@4.62.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.1': + '@rollup/rollup-win32-x64-msvc@4.62.0': optional: true '@rtsao/scc@1.1.0': {} @@ -4459,9 +5302,9 @@ snapshots: '@secretlint/profiler': 10.2.2 '@secretlint/resolver': 10.2.2 '@secretlint/types': 10.2.2 - ajv: 8.18.0 + ajv: 8.20.0 debug: 4.4.3(supports-color@8.1.1) - rc-config-loader: 4.1.3 + rc-config-loader: 4.1.4 transitivePeerDependencies: - supports-color @@ -4478,13 +5321,13 @@ snapshots: dependencies: '@secretlint/resolver': 10.2.2 '@secretlint/types': 10.2.2 - '@textlint/linter-formatter': 15.2.2 - '@textlint/module-interop': 15.2.2 - '@textlint/types': 15.2.2 + '@textlint/linter-formatter': 15.7.1 + '@textlint/module-interop': 15.7.1 + '@textlint/types': 15.7.1 chalk: 5.6.2 debug: 4.4.3(supports-color@8.1.1) pluralize: 8.0.0 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 table: 6.9.0 terminal-link: 4.0.0 transitivePeerDependencies: @@ -4499,7 +5342,7 @@ snapshots: '@secretlint/source-creator': 10.2.2 '@secretlint/types': 10.2.2 debug: 4.4.3(supports-color@8.1.1) - p-map: 7.0.3 + p-map: 7.0.4 transitivePeerDependencies: - supports-color @@ -4509,7 +5352,7 @@ snapshots: '@secretlint/secretlint-formatter-sarif@10.2.2': dependencies: - node-sarif-builder: 3.2.0 + node-sarif-builder: 3.4.0 '@secretlint/secretlint-rule-no-dotenv@10.2.2': dependencies: @@ -4526,18 +5369,66 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} - '@textlint/ast-node-types@15.2.2': {} + '@smithy/core@3.25.1': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 - '@textlint/linter-formatter@15.2.2': + '@smithy/credential-provider-imds@4.4.1': + dependencies: + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.5.1': + dependencies: + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.8.1': + dependencies: + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.5.1': + dependencies: + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/types@4.15.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@textlint/ast-node-types@15.7.1': {} + + '@textlint/linter-formatter@15.7.1': dependencies: '@azu/format-text': 1.0.2 '@azu/style-format': 1.0.1 - '@textlint/module-interop': 15.2.2 - '@textlint/resolver': 15.2.2 - '@textlint/types': 15.2.2 + '@textlint/module-interop': 15.7.1 + '@textlint/resolver': 15.7.1 + '@textlint/types': 15.7.1 chalk: 4.1.2 debug: 4.4.3(supports-color@8.1.1) - js-yaml: 3.14.2 + js-yaml: 4.2.0 lodash: 4.18.1 pluralize: 2.0.0 string-width: 4.2.3 @@ -4547,34 +5438,34 @@ snapshots: transitivePeerDependencies: - supports-color - '@textlint/module-interop@15.2.2': {} + '@textlint/module-interop@15.7.1': {} - '@textlint/resolver@15.2.2': {} + '@textlint/resolver@15.7.1': {} - '@textlint/types@15.2.2': + '@textlint/types@15.7.1': dependencies: - '@textlint/ast-node-types': 15.2.2 + '@textlint/ast-node-types': 15.7.1 '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/d3-color@3.1.3': {} @@ -4597,12 +5488,12 @@ snapshots: '@types/d3-interpolate': 3.0.4 '@types/d3-selection': 3.0.11 - '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 20.19.0 + '@types/node': 20.19.43 '@types/istanbul-lib-coverage@2.0.6': {} @@ -4612,13 +5503,13 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 20.19.0 + '@types/node': 20.19.43 '@types/mocha@10.0.10': {} '@types/ms@2.1.0': {} - '@types/node@20.19.0': + '@types/node@20.19.43': dependencies: undici-types: 6.21.0 @@ -4628,15 +5519,15 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@17.0.26(@types/react@17.0.91)': + '@types/react-dom@17.0.26(@types/react@17.0.93)': dependencies: - '@types/react': 17.0.91 + '@types/react': 17.0.93 - '@types/react-transition-group@4.4.12(@types/react@17.0.91)': + '@types/react-transition-group@4.4.12(@types/react@17.0.93)': dependencies: - '@types/react': 17.0.91 + '@types/react': 17.0.93 - '@types/react@17.0.91': + '@types/react@17.0.93': dependencies: '@types/prop-types': 15.7.15 '@types/scheduler': 0.16.8 @@ -4646,16 +5537,16 @@ snapshots: '@types/scheduler@0.16.8': {} - '@types/vscode@1.103.0': {} + '@types/vscode@1.125.0': {} - '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.59.2 - '@typescript-eslint/type-utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.2 + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/type-utils': 8.61.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 eslint: 9.39.4 ignore: 7.0.5 natural-compare: 1.4.0 @@ -4664,41 +5555,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.59.2 - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.2 + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.4 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.2(typescript@5.9.3)': + '@typescript-eslint/project-service@8.61.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) - '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.59.2': + '@typescript-eslint/scope-manager@8.61.1': dependencies: - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/visitor-keys': 8.59.2 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 - '@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.61.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.59.2(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.61.1(eslint@9.39.4)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4)(typescript@5.9.3) debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.4 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -4706,40 +5597,40 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.59.2': {} + '@typescript-eslint/types@8.61.1': {} - '@typescript-eslint/typescript-estree@8.59.2(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.61.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.59.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/visitor-keys': 8.59.2 + '@typescript-eslint/project-service': 8.61.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.5 - semver: 7.7.4 - tinyglobby: 0.2.16 + semver: 7.8.4 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.2(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/utils@8.61.1(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) - '@typescript-eslint/scope-manager': 8.59.2 - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) eslint: 9.39.4 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.59.2': + '@typescript-eslint/visitor-keys@8.61.1': dependencies: - '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/types': 8.61.1 eslint-visitor-keys: 5.0.1 - '@typespec/ts-http-runtime@0.3.0': + '@typespec/ts-http-runtime@0.3.6': dependencies: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -4747,15 +5638,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@5.2.0(vite@7.3.2(@types/node@20.19.0))': + '@vitejs/plugin-react@5.2.0(vite@7.3.5(@types/node@20.19.43))': dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.2(@types/node@20.19.0) + vite: 7.3.5(@types/node@20.19.43) transitivePeerDependencies: - supports-color @@ -4764,10 +5655,10 @@ snapshots: '@types/mocha': 10.0.10 c8: 10.1.3 chokidar: 3.6.0 - enhanced-resolve: 5.20.1 + enhanced-resolve: 5.24.0 glob: 10.5.0 minimatch: 9.0.9 - mocha: 11.7.5 + mocha: 11.7.6 supports-color: 10.2.2 yargs: 17.7.2 transitivePeerDependencies: @@ -4779,97 +5670,99 @@ snapshots: https-proxy-agent: 7.0.6 jszip: 3.10.1 ora: 8.2.0 - semver: 7.7.4 + semver: 7.8.4 transitivePeerDependencies: - supports-color - '@vscode/vsce-sign-alpine-arm64@2.0.5': + '@vscode/vsce-sign-alpine-arm64@2.0.6': optional: true - '@vscode/vsce-sign-alpine-x64@2.0.5': + '@vscode/vsce-sign-alpine-x64@2.0.6': optional: true - '@vscode/vsce-sign-darwin-arm64@2.0.5': + '@vscode/vsce-sign-darwin-arm64@2.0.6': optional: true - '@vscode/vsce-sign-darwin-x64@2.0.5': + '@vscode/vsce-sign-darwin-x64@2.0.6': optional: true - '@vscode/vsce-sign-linux-arm64@2.0.5': + '@vscode/vsce-sign-linux-arm64@2.0.6': optional: true - '@vscode/vsce-sign-linux-arm@2.0.5': + '@vscode/vsce-sign-linux-arm@2.0.6': optional: true - '@vscode/vsce-sign-linux-x64@2.0.5': + '@vscode/vsce-sign-linux-x64@2.0.6': optional: true - '@vscode/vsce-sign-win32-arm64@2.0.5': + '@vscode/vsce-sign-win32-arm64@2.0.6': optional: true - '@vscode/vsce-sign-win32-x64@2.0.5': + '@vscode/vsce-sign-win32-x64@2.0.6': optional: true - '@vscode/vsce-sign@2.0.6': + '@vscode/vsce-sign@2.0.9': optionalDependencies: - '@vscode/vsce-sign-alpine-arm64': 2.0.5 - '@vscode/vsce-sign-alpine-x64': 2.0.5 - '@vscode/vsce-sign-darwin-arm64': 2.0.5 - '@vscode/vsce-sign-darwin-x64': 2.0.5 - '@vscode/vsce-sign-linux-arm': 2.0.5 - '@vscode/vsce-sign-linux-arm64': 2.0.5 - '@vscode/vsce-sign-linux-x64': 2.0.5 - '@vscode/vsce-sign-win32-arm64': 2.0.5 - '@vscode/vsce-sign-win32-x64': 2.0.5 - - '@vscode/vsce@3.9.1': + '@vscode/vsce-sign-alpine-arm64': 2.0.6 + '@vscode/vsce-sign-alpine-x64': 2.0.6 + '@vscode/vsce-sign-darwin-arm64': 2.0.6 + '@vscode/vsce-sign-darwin-x64': 2.0.6 + '@vscode/vsce-sign-linux-arm': 2.0.6 + '@vscode/vsce-sign-linux-arm64': 2.0.6 + '@vscode/vsce-sign-linux-x64': 2.0.6 + '@vscode/vsce-sign-win32-arm64': 2.0.6 + '@vscode/vsce-sign-win32-x64': 2.0.6 + + '@vscode/vsce@3.9.2': dependencies: '@azure/identity': 4.13.1 '@secretlint/node': 10.2.2 '@secretlint/secretlint-formatter-sarif': 10.2.2 '@secretlint/secretlint-rule-no-dotenv': 10.2.2 '@secretlint/secretlint-rule-preset-recommend': 10.2.2 - '@vscode/vsce-sign': 2.0.6 + '@vscode/vsce-sign': 2.0.9 azure-devops-node-api: 12.5.0 chalk: 4.1.2 - cheerio: 1.1.2 + cheerio: 1.2.0 cockatiel: 3.2.1 commander: 12.1.0 - form-data: 4.0.4 - glob: 11.1.0 + form-data: 4.0.6 + glob: 13.0.6 hosted-git-info: 4.1.0 jsonc-parser: 3.3.1 leven: 3.1.0 - markdown-it: 14.1.1 + markdown-it: 14.2.0 mime: 1.6.0 - minimatch: 3.1.5 + minimatch: 10.2.5 parse-semver: 1.1.1 read: 1.0.7 secretlint: 10.2.2 - semver: 7.7.4 - tmp: 0.2.5 + semver: 7.8.4 + tmp: 0.2.7 typed-rest-client: 1.8.11 url-join: 4.0.1 xml2js: 0.5.0 - yauzl: 3.3.0 + yauzl: 3.4.0 yazl: 2.5.1 optionalDependencies: keytar: 7.9.0 transitivePeerDependencies: - supports-color - '@xyflow/react@12.10.2(@types/react@17.0.91)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@xyflow/react@12.11.0(@types/react-dom@17.0.26(@types/react@17.0.93))(@types/react@17.0.93)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: - '@xyflow/system': 0.0.76 + '@xyflow/system': 0.0.77 classcat: 5.0.5 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - zustand: 4.5.7(@types/react@17.0.91)(react@17.0.2) + zustand: 4.5.7(@types/react@17.0.93)(react@17.0.2) + optionalDependencies: + '@types/react': 17.0.93 + '@types/react-dom': 17.0.26(@types/react@17.0.93) transitivePeerDependencies: - - '@types/react' - immer - '@xyflow/system@0.0.76': + '@xyflow/system@0.0.77': dependencies: '@types/d3-drag': 3.0.7 '@types/d3-interpolate': 3.0.4 @@ -4881,29 +5774,29 @@ snapshots: d3-selection: 3.0.0 d3-zoom: 3.0.0 - acorn-jsx@5.3.2(acorn@8.16.0): + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: - acorn: 8.16.0 + acorn: 8.17.0 - acorn@8.16.0: {} + acorn@8.17.0: {} - agent-base@7.1.3: {} + agent-base@7.1.4: {} - ajv@6.14.0: + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.18.0: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - ansi-escapes@7.0.0: + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -4926,9 +5819,7 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 + anynum@1.0.0: {} argparse@2.0.1: {} @@ -4939,45 +5830,45 @@ snapshots: array-includes@3.1.9: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 is-string: 1.1.1 math-intrinsics: 1.1.0 array.prototype.findlastindex@1.2.6: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-shim-unscopables: 1.1.0 array.prototype.flat@1.3.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-shim-unscopables: 1.1.0 arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 @@ -4999,7 +5890,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 cosmiconfig: 7.1.0 resolve: 1.22.12 @@ -5010,7 +5901,7 @@ snapshots: base64-js@1.5.1: optional: true - baseline-browser-mapping@2.10.13: {} + baseline-browser-mapping@2.10.38: {} binary-extensions@2.3.0: {} @@ -5029,16 +5920,18 @@ snapshots: boundary@2.0.0: {} - brace-expansion@1.1.14: + bowser@2.14.1: {} + + brace-expansion@1.1.15: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.1.0: + brace-expansion@2.1.1: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.5: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -5050,10 +5943,10 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.13 - caniuse-lite: 1.0.30001784 - electron-to-chromium: 1.5.330 - node-releases: 2.0.36 + baseline-browser-mapping: 2.10.38 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.375 + node-releases: 2.0.47 update-browserslist-db: 1.2.3(browserslist@4.28.2) buffer-crc32@0.2.13: {} @@ -5068,12 +5961,12 @@ snapshots: bundle-name@4.1.0: dependencies: - run-applescript: 7.0.0 + run-applescript: 7.1.0 c8@10.1.3: dependencies: '@bcoe/v8-coverage': 1.0.2 - '@istanbuljs/schema': 0.1.3 + '@istanbuljs/schema': 0.1.6 find-up: 5.0.0 foreground-child: 3.3.1 istanbul-lib-coverage: 3.2.2 @@ -5089,7 +5982,7 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.8: + call-bind@1.0.9: dependencies: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 @@ -5105,7 +5998,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001784: {} + caniuse-lite@1.0.30001799: {} chalk@2.4.2: dependencies: @@ -5129,18 +6022,18 @@ snapshots: domhandler: 5.0.3 domutils: 3.2.2 - cheerio@1.1.2: + cheerio@1.2.0: dependencies: cheerio-select: 2.1.0 dom-serializer: 2.0.0 domhandler: 5.0.3 domutils: 3.2.2 encoding-sniffer: 0.2.1 - htmlparser2: 10.0.0 + htmlparser2: 10.1.0 parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.24.5 + undici: 7.28.0 whatwg-mimetype: 4.0.0 chokidar@3.6.0: @@ -5238,7 +6131,7 @@ snapshots: css-vendor@2.0.8: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 is-in-browser: 1.1.3 css-what@6.2.2: {} @@ -5321,12 +6214,12 @@ snapshots: deep-is@0.1.4: {} - default-browser-id@5.0.0: {} + default-browser-id@5.0.1: {} - default-browser@5.2.1: + default-browser@5.5.0: dependencies: bundle-name: 4.1.0 - default-browser-id: 5.0.0 + default-browser-id: 5.0.1 define-data-property@1.1.4: dependencies: @@ -5344,7 +6237,7 @@ snapshots: delayed-stream@1.0.0: {} - detect-libc@2.0.4: + detect-libc@2.1.2: optional: true diff@8.0.4: {} @@ -5355,7 +6248,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 csstype: 3.2.3 dom-serializer@2.0.0: @@ -5386,15 +6279,15 @@ snapshots: ecdsa-sig-formatter@1.0.11: dependencies: - safe-buffer: 5.1.2 + safe-buffer: 5.2.1 editions@6.22.0: dependencies: version-range: 4.15.0 - electron-to-chromium@1.5.330: {} + electron-to-chromium@1.5.375: {} - emoji-regex@10.5.0: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -5410,41 +6303,46 @@ snapshots: once: 1.4.0 optional: true - enhanced-resolve@5.20.1: + enhanced-resolve@5.24.0: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.2 + tapable: 2.3.3 entities@4.5.0: {} entities@6.0.1: {} + entities@7.0.1: {} + environment@1.1.0: {} - error-ex@1.3.2: + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 - error-ex@1.3.4: + es-abstract-get@1.0.0: dependencies: - is-arrayish: 0.2.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 - es-abstract@1.24.0: + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 available-typed-arrays: 1.0.7 - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 data-view-buffer: 1.0.2 data-view-byte-length: 1.0.2 data-view-byte-offset: 1.0.1 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 + es-to-primitive: 1.3.1 + function.prototype.name: 1.2.0 get-intrinsic: 1.3.0 get-proto: 1.0.1 get-symbol-description: 1.1.0 @@ -5453,7 +6351,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -5471,26 +6369,26 @@ snapshots: object.assign: 4.1.7 own-keys: 1.0.1 regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 + safe-array-concat: 1.1.4 safe-push-apply: 1.0.0 safe-regex-test: 1.1.0 set-proto: 1.0.0 stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 string.prototype.trimstart: 1.0.8 typed-array-buffer: 1.0.3 typed-array-byte-length: 1.0.3 typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 + typed-array-length: 1.0.8 unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 + which-typed-array: 1.1.22 es-define-property@1.0.1: {} es-errors@1.3.0: {} - es-object-atoms@1.1.1: + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -5499,14 +6397,16 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 es-shim-unscopables@1.1.0: dependencies: - hasown: 2.0.2 + hasown: 2.0.4 - es-to-primitive@1.3.0: + es-to-primitive@1.3.1: dependencies: + es-abstract-get: 1.0.0 + es-errors: 1.3.0 is-callable: 1.2.7 is-date-object: 1.1.0 is-symbol: 1.1.1 @@ -5540,34 +6440,34 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.0: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.28.0 - '@esbuild/android-arm': 0.28.0 - '@esbuild/android-arm64': 0.28.0 - '@esbuild/android-x64': 0.28.0 - '@esbuild/darwin-arm64': 0.28.0 - '@esbuild/darwin-x64': 0.28.0 - '@esbuild/freebsd-arm64': 0.28.0 - '@esbuild/freebsd-x64': 0.28.0 - '@esbuild/linux-arm': 0.28.0 - '@esbuild/linux-arm64': 0.28.0 - '@esbuild/linux-ia32': 0.28.0 - '@esbuild/linux-loong64': 0.28.0 - '@esbuild/linux-mips64el': 0.28.0 - '@esbuild/linux-ppc64': 0.28.0 - '@esbuild/linux-riscv64': 0.28.0 - '@esbuild/linux-s390x': 0.28.0 - '@esbuild/linux-x64': 0.28.0 - '@esbuild/netbsd-arm64': 0.28.0 - '@esbuild/netbsd-x64': 0.28.0 - '@esbuild/openbsd-arm64': 0.28.0 - '@esbuild/openbsd-x64': 0.28.0 - '@esbuild/openharmony-arm64': 0.28.0 - '@esbuild/sunos-x64': 0.28.0 - '@esbuild/win32-arm64': 0.28.0 - '@esbuild/win32-ia32': 0.28.0 - '@esbuild/win32-x64': 0.28.0 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -5575,25 +6475,25 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-import-resolver-node@0.3.9: + eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.10 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4): + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4)(typescript@5.9.3) eslint: 9.39.4 - eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -5603,20 +6503,20 @@ snapshots: debug: 3.2.7 doctrine: 2.1.0 eslint: 9.39.4 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4) - hasown: 2.0.2 - is-core-module: 2.16.1 + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4) + hasown: 2.0.4 + is-core-module: 2.16.2 is-glob: 4.0.3 minimatch: 3.1.5 object.fromentries: 2.0.8 object.groupby: 1.0.3 object.values: 1.2.1 semver: 6.3.1 - string.prototype.trimend: 1.0.9 + string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5643,11 +6543,11 @@ snapshots: '@eslint/eslintrc': 3.3.5 '@eslint/js': 9.39.4 '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.6 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.14.0 + '@types/estree': 1.0.9 + ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3(supports-color@8.1.1) @@ -5655,7 +6555,7 @@ snapshots: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - esquery: 1.6.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -5674,13 +6574,11 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) eslint-visitor-keys: 4.2.1 - esprima@4.0.1: {} - - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -5711,7 +6609,19 @@ snapshots: fast-uri@3.1.2: {} - fastq@1.19.1: + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 + + fast-xml-parser@5.7.3: + dependencies: + '@nodable/entities': 2.2.0 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.4.0 + + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -5752,21 +6662,21 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.4: + form-data@4.0.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.4 mime-types: 2.1.35 fs-constants@1.0.0: optional: true - fs-extra@11.3.4: + fs-extra@11.3.5: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.1 universalify: 2.0.1 fsevents@2.3.3: @@ -5774,40 +6684,45 @@ snapshots: function-bind@1.1.2: {} - function.prototype.name@1.1.8: + function.prototype.name@1.2.0: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 - define-properties: 1.2.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 functions-have-names: 1.2.3 - hasown: 2.0.2 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 is-callable: 1.2.7 + is-document.all: 1.0.0 functions-have-names@1.2.3: {} + generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} - get-east-asian-width@1.3.1: {} + get-east-asian-width@1.6.0: {} get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-symbol-description@1.1.0: dependencies: @@ -5831,18 +6746,15 @@ snapshots: foreground-child: 3.3.1 jackspeak: 3.4.3 minimatch: 9.0.9 - minipass: 7.1.2 + minipass: 7.1.3 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@11.1.0: + glob@13.0.6: dependencies: - foreground-child: 3.3.1 - jackspeak: 4.1.1 minimatch: 10.2.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 2.0.0 + minipass: 7.1.3 + path-scurry: 2.0.2 globals@14.0.0: {} @@ -5884,11 +6796,7 @@ snapshots: dependencies: has-symbols: 1.1.0 - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hasown@2.0.3: + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -5910,23 +6818,23 @@ snapshots: html-escaper@2.0.2: {} - htmlparser2@10.0.0: + htmlparser2@10.1.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 domutils: 3.2.2 - entities: 6.0.1 + entities: 7.0.1 http-proxy-agent@7.0.2: dependencies: - agent-base: 7.1.3 + agent-base: 7.1.4 debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: - agent-base: 7.1.3 + agent-base: 7.1.4 debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -5953,7 +6861,7 @@ snapshots: imurmurhash@0.1.4: {} - index-to-position@1.1.0: {} + index-to-position@1.2.0: {} inherits@2.0.4: {} @@ -5963,12 +6871,12 @@ snapshots: internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 + hasown: 2.0.4 + side-channel: 1.1.1 is-array-buffer@3.0.5: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 get-intrinsic: 1.3.0 @@ -5997,13 +6905,9 @@ snapshots: is-callable@1.2.7: {} - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - is-core-module@2.16.2: dependencies: - hasown: 2.0.3 + hasown: 2.0.4 is-data-view@1.0.2: dependencies: @@ -6018,6 +6922,10 @@ snapshots: is-docker@3.0.0: {} + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -6026,9 +6934,10 @@ snapshots: is-fullwidth-code-point@3.0.0: {} - is-generator-function@1.1.0: + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 + generator-function: 2.0.1 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -6065,7 +6974,7 @@ snapshots: call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 is-set@2.0.3: {} @@ -6086,7 +6995,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.19 + which-typed-array: 1.1.22 is-unicode-supported@0.1.0: {} @@ -6105,7 +7014,7 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-wsl@3.1.0: + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -6140,18 +7049,11 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jackspeak@4.1.1: - dependencies: - '@isaacs/cliui': 8.0.2 + js-ini@1.6.0: {} js-tokens@4.0.0: {} - js-yaml@3.14.2: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.1: + js-yaml@4.2.0: dependencies: argparse: 2.0.1 @@ -6177,7 +7079,7 @@ snapshots: jsonc-parser@3.3.1: {} - jsonfile@6.1.0: + jsonfile@6.2.1: dependencies: universalify: 2.0.1 optionalDependencies: @@ -6194,50 +7096,50 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.4 + semver: 7.8.4 jss-plugin-camel-case@10.10.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 hyphenate-style-name: 1.1.0 jss: 10.10.0 jss-plugin-default-unit@10.10.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 jss: 10.10.0 jss-plugin-global@10.10.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 jss: 10.10.0 jss-plugin-nested@10.10.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 jss: 10.10.0 tiny-warning: 1.0.3 jss-plugin-props-sort@10.10.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 jss: 10.10.0 jss-plugin-rule-value-function@10.10.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 jss: 10.10.0 tiny-warning: 1.0.3 jss-plugin-vendor-prefixer@10.10.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 css-vendor: 2.0.8 jss: 10.10.0 jss@10.10.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 csstype: 3.2.3 is-in-browser: 1.1.3 tiny-warning: 1.0.3 @@ -6253,12 +7155,12 @@ snapshots: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.1.2 + safe-buffer: 5.2.1 jws@4.0.1: dependencies: jwa: 2.0.1 - safe-buffer: 5.1.2 + safe-buffer: 5.2.1 keytar@7.9.0: dependencies: @@ -6283,7 +7185,7 @@ snapshots: lines-and-columns@1.2.4: {} - linkify-it@5.0.0: + linkify-it@5.0.1: dependencies: uc.micro: 2.1.0 @@ -6334,7 +7236,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.1.0: {} + lru-cache@11.5.1: {} lru-cache@5.1.1: dependencies: @@ -6346,13 +7248,13 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.4 - markdown-it@14.1.1: + markdown-it@14.2.0: dependencies: argparse: 2.0.1 entities: 4.5.0 - linkify-it: 5.0.0 + linkify-it: 5.0.1 mdurl: 2.0.0 punycode.js: 2.3.1 uc.micro: 2.1.0 @@ -6385,24 +7287,28 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.14 + brace-expansion: 1.1.15 minimatch@9.0.9: dependencies: - brace-expansion: 2.1.0 + brace-expansion: 2.1.1 minimist@1.2.8: {} - minipass@7.1.2: {} + minipass@7.1.3: {} mkdirp-classic@0.5.3: optional: true - mocha@11.7.5: + mnemonist@0.38.3: + dependencies: + obliterator: 1.6.1 + + mocha@11.7.6: dependencies: browser-stdout: 1.3.1 chokidar: 4.0.3 @@ -6413,12 +7319,12 @@ snapshots: glob: 10.5.0 he: 1.2.0 is-path-inside: 3.0.3 - js-yaml: 4.1.1 + js-yaml: 4.2.0 log-symbols: 4.1.0 minimatch: 9.0.9 ms: 2.1.3 picocolors: 1.1.1 - serialize-javascript: 7.0.5 + serialize-javascript: 7.0.6 strip-json-comments: 3.1.1 supports-color: 8.1.1 workerpool: 9.3.4 @@ -6430,7 +7336,7 @@ snapshots: mute-stream@0.0.8: {} - nanoid@3.3.11: {} + nanoid@3.3.12: {} napi-build-utils@2.0.0: optional: true @@ -6439,32 +7345,39 @@ snapshots: nice-try@1.0.5: {} - node-abi@3.75.0: + node-abi@3.92.0: dependencies: - semver: 7.7.4 + semver: 7.8.4 optional: true node-addon-api@4.3.0: optional: true - node-releases@2.0.36: {} + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-releases@2.0.47: {} - node-sarif-builder@3.2.0: + node-sarif-builder@3.4.0: dependencies: '@types/sarif': 2.1.7 - fs-extra: 11.3.4 + fs-extra: 11.3.5 normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.10 + resolve: 1.22.12 semver: 5.7.2 validate-npm-package-license: 3.0.4 normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 - semver: 7.7.4 + semver: 7.8.4 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -6478,7 +7391,7 @@ snapshots: minimatch: 3.1.5 pidtree: 0.3.1 read-pkg: 3.0.0 - shell-quote: 1.8.3 + shell-quote: 1.8.4 string.prototype.padend: 3.1.6 nth-check@2.1.1: @@ -6493,32 +7406,41 @@ snapshots: object.assign@4.1.7: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 has-symbols: 1.1.0 object-keys: 1.1.1 + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + object.fromentries@2.0.8: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 object.groupby@1.0.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 object.values@1.2.1: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 + + obliterator@1.6.1: {} once@1.4.0: dependencies: @@ -6531,7 +7453,7 @@ snapshots: open@10.2.0: dependencies: - default-browser: 5.2.1 + default-browser: 5.5.0 define-lazy-prop: 3.0.0 is-inside-container: 1.0.0 wsl-utils: 0.1.0 @@ -6555,7 +7477,7 @@ snapshots: log-symbols: 6.0.0 stdin-discarder: 0.2.2 string-width: 7.2.0 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 own-keys@1.0.1: dependencies: @@ -6571,7 +7493,7 @@ snapshots: dependencies: p-limit: 3.1.0 - p-map@7.0.3: {} + p-map@7.0.4: {} p-min-delay@4.2.0: dependencies: @@ -6587,20 +7509,20 @@ snapshots: parse-json@4.0.0: dependencies: - error-ex: 1.3.2 + error-ex: 1.3.4 json-parse-better-errors: 1.0.2 parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 parse-json@8.3.0: dependencies: - '@babel/code-frame': 7.29.0 - index-to-position: 1.1.0 + '@babel/code-frame': 7.29.7 + index-to-position: 1.2.0 type-fest: 4.41.0 parse-semver@1.1.1: @@ -6622,6 +7544,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.5.0: {} + path-key@2.0.1: {} path-key@3.1.1: {} @@ -6631,12 +7555,12 @@ snapshots: path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 - minipass: 7.1.2 + minipass: 7.1.3 - path-scurry@2.0.0: + path-scurry@2.0.2: dependencies: - lru-cache: 11.1.0 - minipass: 7.1.2 + lru-cache: 11.5.1 + minipass: 7.1.3 path-type@3.0.0: dependencies: @@ -6664,22 +7588,22 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.12: + postcss@8.5.15: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 prebuild-install@7.1.3: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 expand-template: 2.0.3 github-from-package: 0.0.0 minimist: 1.2.8 mkdirp-classic: 0.5.3 napi-build-utils: 2.0.0 - node-abi: 3.75.0 - pump: 3.0.3 + node-abi: 3.92.0 + pump: 3.0.4 rc: 1.2.8 simple-get: 4.0.1 tar-fs: 2.1.4 @@ -6696,7 +7620,7 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - pump@3.0.3: + pump@3.0.4: dependencies: end-of-stream: 1.4.5 once: 1.4.0 @@ -6706,16 +7630,16 @@ snapshots: punycode@2.3.1: {} - qs@6.15.0: + qs@6.15.2: dependencies: - side-channel: 1.1.0 + side-channel: 1.1.1 queue-microtask@1.2.3: {} - rc-config-loader@4.1.3: + rc-config-loader@4.1.4: dependencies: debug: 4.4.3(supports-color@8.1.1) - js-yaml: 4.1.1 + js-yaml: 4.2.0 json5: 2.2.3 require-from-string: 2.0.2 transitivePeerDependencies: @@ -6738,25 +7662,25 @@ snapshots: react-is@16.13.1: {} - react-is@19.2.4: {} + react-is@19.2.7: {} react-refresh@0.18.0: {} - react-router-dom@6.30.3(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + react-router-dom@6.30.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: - '@remix-run/router': 1.23.2 + '@remix-run/router': 1.23.3 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - react-router: 6.30.3(react@17.0.2) + react-router: 6.30.4(react@17.0.2) - react-router@6.30.3(react@17.0.2): + react-router@6.30.4(react@17.0.2): dependencies: - '@remix-run/router': 1.23.2 + '@remix-run/router': 1.23.3 react: 17.0.2 react-transition-group@4.4.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -6799,7 +7723,7 @@ snapshots: readable-stream@3.6.2: dependencies: inherits: 2.0.4 - string_decoder: 1.1.1 + string_decoder: 1.3.0 util-deprecate: 1.0.2 optional: true @@ -6811,18 +7735,18 @@ snapshots: reflect.getprototypeof@1.0.10: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 regexp.prototype.flags@1.5.4: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 es-errors: 1.3.0 get-proto: 1.0.1 @@ -6835,16 +7759,19 @@ snapshots: resolve-from@4.0.0: {} - resolve@1.22.10: + resolve@1.22.12: dependencies: - is-core-module: 2.16.1 + es-errors: 1.3.0 + is-core-module: 2.16.2 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.12: + resolve@2.0.0-next.7: dependencies: es-errors: 1.3.0 is-core-module: 2.16.2 + node-exports-info: 1.6.0 + object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -6855,46 +7782,46 @@ snapshots: reusify@1.1.0: {} - rollup@4.60.1: + rollup@4.62.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 + '@rollup/rollup-android-arm-eabi': 4.62.0 + '@rollup/rollup-android-arm64': 4.62.0 + '@rollup/rollup-darwin-arm64': 4.62.0 + '@rollup/rollup-darwin-x64': 4.62.0 + '@rollup/rollup-freebsd-arm64': 4.62.0 + '@rollup/rollup-freebsd-x64': 4.62.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.0 + '@rollup/rollup-linux-arm-musleabihf': 4.62.0 + '@rollup/rollup-linux-arm64-gnu': 4.62.0 + '@rollup/rollup-linux-arm64-musl': 4.62.0 + '@rollup/rollup-linux-loong64-gnu': 4.62.0 + '@rollup/rollup-linux-loong64-musl': 4.62.0 + '@rollup/rollup-linux-ppc64-gnu': 4.62.0 + '@rollup/rollup-linux-ppc64-musl': 4.62.0 + '@rollup/rollup-linux-riscv64-gnu': 4.62.0 + '@rollup/rollup-linux-riscv64-musl': 4.62.0 + '@rollup/rollup-linux-s390x-gnu': 4.62.0 + '@rollup/rollup-linux-x64-gnu': 4.62.0 + '@rollup/rollup-linux-x64-musl': 4.62.0 + '@rollup/rollup-openbsd-x64': 4.62.0 + '@rollup/rollup-openharmony-arm64': 4.62.0 + '@rollup/rollup-win32-arm64-msvc': 4.62.0 + '@rollup/rollup-win32-ia32-msvc': 4.62.0 + '@rollup/rollup-win32-x64-gnu': 4.62.0 + '@rollup/rollup-win32-x64-msvc': 4.62.0 fsevents: 2.3.3 - run-applescript@7.0.0: {} + run-applescript@7.1.0: {} run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - safe-array-concat@1.1.3: + safe-array-concat@1.1.4: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 get-intrinsic: 1.3.0 has-symbols: 1.1.0 @@ -6902,6 +7829,8 @@ snapshots: safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -6915,7 +7844,7 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.4.1: {} + sax@1.6.0: {} scheduler@0.20.2: dependencies: @@ -6938,9 +7867,9 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} + semver@7.8.4: {} - serialize-javascript@7.0.5: {} + serialize-javascript@7.0.6: {} set-function-length@1.2.2: dependencies: @@ -6962,7 +7891,7 @@ snapshots: dependencies: dunder-proto: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 setimmediate@1.0.5: {} @@ -6978,9 +7907,9 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.3: {} + shell-quote@1.8.4: {} - side-channel-list@1.0.0: + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -7000,11 +7929,11 @@ snapshots: object-inspect: 1.13.4 side-channel-map: 1.0.1 - side-channel@1.1.0: + side-channel@1.1.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 - side-channel-list: 1.0.0 + side-channel-list: 1.0.1 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 @@ -7035,18 +7964,16 @@ snapshots: spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.21 + spdx-license-ids: 3.0.23 spdx-exceptions@2.5.0: {} spdx-expression-parse@3.0.1: dependencies: spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.21 - - spdx-license-ids@3.0.21: {} + spdx-license-ids: 3.0.23 - sprintf-js@1.0.3: {} + spdx-license-ids@3.0.23: {} stdin-discarder@0.2.2: {} @@ -7065,53 +7992,59 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 string-width@7.2.0: dependencies: - emoji-regex: 10.5.0 - get-east-asian-width: 1.3.1 - strip-ansi: 7.1.2 + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 string.prototype.padend@3.1.6: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 - string.prototype.trim@1.2.10: + string.prototype.trim@1.2.11: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 - string.prototype.trimend@1.0.9: + string.prototype.trimend@1.0.10: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string.prototype.trimstart@1.0.8: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.2: + strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 @@ -7122,6 +8055,10 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@2.4.0: + dependencies: + anynum: 1.0.0 + structured-source@4.0.0: dependencies: boundary: 2.0.0 @@ -7151,19 +8088,19 @@ snapshots: table@6.9.0: dependencies: - ajv: 8.18.0 + ajv: 8.20.0 lodash.truncate: 4.4.2 slice-ansi: 4.0.0 string-width: 4.2.3 strip-ansi: 6.0.1 - tapable@2.3.2: {} + tapable@2.3.3: {} tar-fs@2.1.4: dependencies: chownr: 1.1.4 mkdirp-classic: 0.5.3 - pump: 3.0.3 + pump: 3.0.4 tar-stream: 2.2.0 optional: true @@ -7178,12 +8115,12 @@ snapshots: terminal-link@4.0.0: dependencies: - ansi-escapes: 7.0.0 + ansi-escapes: 7.3.0 supports-hyperlinks: 3.2.0 test-exclude@7.0.2: dependencies: - '@istanbuljs/schema': 0.1.3 + '@istanbuljs/schema': 0.1.6 glob: 10.5.0 minimatch: 10.2.5 @@ -7195,12 +8132,12 @@ snapshots: tiny-warning@1.0.3: {} - tinyglobby@0.2.16: + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tmp@0.2.5: {} + tmp@0.2.7: {} to-regex-range@5.0.1: dependencies: @@ -7221,7 +8158,7 @@ snapshots: tunnel-agent@0.6.0: dependencies: - safe-buffer: 5.1.2 + safe-buffer: 5.2.1 optional: true tunnel@0.0.6: {} @@ -7240,7 +8177,7 @@ snapshots: typed-array-byte-length@1.0.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 @@ -7249,16 +8186,16 @@ snapshots: typed-array-byte-offset@1.0.4: dependencies: available-typed-arrays: 1.0.7 - call-bind: 1.0.8 + call-bind: 1.0.9 for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 reflect.getprototypeof: 1.0.10 - typed-array-length@1.0.7: + typed-array-length@1.0.8: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 for-each: 0.3.5 gopd: 1.2.0 is-typed-array: 1.1.15 @@ -7267,16 +8204,16 @@ snapshots: typed-rest-client@1.8.11: dependencies: - qs: 6.15.0 + qs: 6.15.2 tunnel: 0.0.6 underscore: 1.13.8 - typescript-eslint@8.59.2(eslint@9.39.4)(typescript@5.9.3): + typescript-eslint@8.61.1(eslint@9.39.4)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4)(typescript@5.9.3) eslint: 9.39.4 typescript: 5.9.3 transitivePeerDependencies: @@ -7297,7 +8234,7 @@ snapshots: undici-types@6.21.0: {} - undici@7.24.5: {} + undici@7.28.0: {} unicorn-magic@0.1.0: {} @@ -7338,16 +8275,16 @@ snapshots: version-range@4.15.0: {} - vite@7.3.2(@types/node@20.19.0): + vite@7.3.5(@types/node@20.19.43): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.12 - rollup: 4.60.1 - tinyglobby: 0.2.16 + postcss: 8.5.15 + rollup: 4.62.0 + tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 20.19.0 + '@types/node': 20.19.43 fsevents: 2.3.3 whatwg-encoding@3.1.1: @@ -7367,18 +8304,18 @@ snapshots: which-builtin-type@1.2.1: dependencies: call-bound: 1.0.4 - function.prototype.name: 1.1.8 + function.prototype.name: 1.2.0 has-tostringtag: 1.0.2 is-async-function: 2.1.1 is-date-object: 1.1.0 is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.0 + is-generator-function: 1.1.2 is-regex: 1.2.1 is-weakref: 1.1.1 isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.19 + which-typed-array: 1.1.22 which-collection@1.0.2: dependencies: @@ -7387,10 +8324,10 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.19: + which-typed-array@1.1.22: dependencies: available-typed-arrays: 1.0.7 - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 for-each: 0.3.5 get-proto: 1.0.1 @@ -7419,18 +8356,20 @@ snapshots: dependencies: ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 wrappy@1.0.2: optional: true wsl-utils@0.1.0: dependencies: - is-wsl: 3.1.0 + is-wsl: 3.1.1 + + xml-naming@0.1.0: {} xml2js@0.5.0: dependencies: - sax: 1.4.1 + sax: 1.6.0 xmlbuilder: 11.0.1 xmlbuilder@11.0.1: {} @@ -7462,9 +8401,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yauzl@3.3.0: + yauzl@3.4.0: dependencies: - buffer-crc32: 0.2.13 pend: 1.2.0 yazl@2.5.1: @@ -7477,9 +8415,9 @@ snapshots: zod@4.4.3: {} - zustand@4.5.7(@types/react@17.0.91)(react@17.0.2): + zustand@4.5.7(@types/react@17.0.93)(react@17.0.2): dependencies: use-sync-external-store: 1.6.0(react@17.0.2) optionalDependencies: - '@types/react': 17.0.91 + '@types/react': 17.0.93 react: 17.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bc8bfd4..f4938e5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,11 @@ minimumReleaseAgeExclude: - serialize-javascript - fast-uri trustPolicy: no-downgrade +trustPolicyExclude: + - chokidar@4.0.3 + - semver@5.7.2 + - semver@6.3.1 + - undici-types@6.21.0 blockExoticSubdeps: true allowBuilds: diff --git a/resources/icons/localstack.svg b/resources/icons/localstack.svg index 3bbb3d1..c926eb2 100644 --- a/resources/icons/localstack.svg +++ b/resources/icons/localstack.svg @@ -1,4 +1,5 @@ + LocalStack diff --git a/resources/service-manifest.json b/resources/service-manifest.json new file mode 100644 index 0000000..9878d8e --- /dev/null +++ b/resources/service-manifest.json @@ -0,0 +1,469 @@ +{ + "$comment": "Generated by build/generate-service-manifest.mjs from localstack-docs coverage data. Regenerate on demand only (when noticed out of date); do not edit by hand. Availability (community/pro) is intentionally omitted.", + "services": [ + { + "id": "account", + "name": "AWS Account Management" + }, + { + "id": "acm", + "name": "ACM" + }, + { + "id": "acm-pca", + "name": "AWS Private Certificate Authority (CA)" + }, + { + "id": "amplify", + "name": "Amplify" + }, + { + "id": "apigateway", + "name": "API Gateway" + }, + { + "id": "apigatewaymanagementapi", + "name": "API Gateway Management API" + }, + { + "id": "apigatewayv2", + "name": "API Gateway v2" + }, + { + "id": "appconfig", + "name": "AppConfig" + }, + { + "id": "appconfigdata", + "name": "AppConfig Data" + }, + { + "id": "application-autoscaling", + "name": "Application Auto Scaling" + }, + { + "id": "appsync", + "name": "AppSync" + }, + { + "id": "athena", + "name": "Athena" + }, + { + "id": "autoscaling", + "name": "Auto Scaling" + }, + { + "id": "backup", + "name": "Backup" + }, + { + "id": "batch", + "name": "Batch" + }, + { + "id": "bedrock", + "name": "Bedrock" + }, + { + "id": "bedrock-runtime", + "name": "Bedrock Runtime" + }, + { + "id": "ce", + "name": "CE" + }, + { + "id": "cloudcontrol", + "name": "Cloudcontrol" + }, + { + "id": "cloudformation", + "name": "CloudFormation" + }, + { + "id": "cloudfront", + "name": "CloudFront" + }, + { + "id": "cloudtrail", + "name": "CloudTrail" + }, + { + "id": "cloudwatch", + "name": "CloudWatch" + }, + { + "id": "codeartifact", + "name": "Codeartifact" + }, + { + "id": "codebuild", + "name": "Codebuild" + }, + { + "id": "codecommit", + "name": "CodeCommit" + }, + { + "id": "codeconnections", + "name": "Codeconnections" + }, + { + "id": "codedeploy", + "name": "Codedeploy" + }, + { + "id": "codepipeline", + "name": "Codepipeline" + }, + { + "id": "codestar-connections", + "name": "Codestar Connections" + }, + { + "id": "cognito-identity", + "name": "Cognito Identity" + }, + { + "id": "cognito-idp", + "name": "Cognito IDP" + }, + { + "id": "config", + "name": "Config" + }, + { + "id": "dms", + "name": "DMS" + }, + { + "id": "docdb", + "name": "DocumentDB" + }, + { + "id": "dynamodb", + "name": "DynamoDB" + }, + { + "id": "dynamodbstreams", + "name": "DynamoDB Streams" + }, + { + "id": "ec2", + "name": "EC2" + }, + { + "id": "ecr", + "name": "ECR" + }, + { + "id": "ecs", + "name": "ECS" + }, + { + "id": "efs", + "name": "EFS" + }, + { + "id": "eks", + "name": "EKS" + }, + { + "id": "eks-auth", + "name": "Eks Auth" + }, + { + "id": "elasticache", + "name": "ElastiCache" + }, + { + "id": "elasticbeanstalk", + "name": "Elastic Beanstalk" + }, + { + "id": "elastictranscoder", + "name": "Elastictranscoder" + }, + { + "id": "elb", + "name": "ELB" + }, + { + "id": "elbv2", + "name": "ELB v2" + }, + { + "id": "emr", + "name": "EMR" + }, + { + "id": "emr-serverless", + "name": "EMR" + }, + { + "id": "es", + "name": "ES" + }, + { + "id": "events", + "name": "EventBridge" + }, + { + "id": "firehose", + "name": "Kinesis Data Firehose" + }, + { + "id": "fis", + "name": "FIS" + }, + { + "id": "glacier", + "name": "Glacier" + }, + { + "id": "glue", + "name": "Glue" + }, + { + "id": "iam", + "name": "IAM" + }, + { + "id": "identitystore", + "name": "Identitystore" + }, + { + "id": "iot", + "name": "IoT" + }, + { + "id": "iot-data", + "name": "IoT Data" + }, + { + "id": "iotwireless", + "name": "IoT Wireless" + }, + { + "id": "kafka", + "name": "MSK" + }, + { + "id": "kinesis", + "name": "Kinesis" + }, + { + "id": "kinesisanalytics", + "name": "Kinesis Data Analytics API" + }, + { + "id": "kinesisanalyticsv2", + "name": "MSF" + }, + { + "id": "kms", + "name": "KMS" + }, + { + "id": "lakeformation", + "name": "Lake Formation" + }, + { + "id": "lambda", + "name": "Lambda" + }, + { + "id": "logs", + "name": "CloudWatch Logs" + }, + { + "id": "managedblockchain", + "name": "Managedblockchain" + }, + { + "id": "mediaconvert", + "name": "Mediaconvert" + }, + { + "id": "memorydb", + "name": "MemoryDB for Redis" + }, + { + "id": "mq", + "name": "Amazon MQ" + }, + { + "id": "mwaa", + "name": "MWAA" + }, + { + "id": "neptune", + "name": "Neptune" + }, + { + "id": "opensearch", + "name": "OpenSearch" + }, + { + "id": "organizations", + "name": "Organizations" + }, + { + "id": "pinpoint", + "name": "Pinpoint" + }, + { + "id": "pipes", + "name": "Pipes" + }, + { + "id": "ram", + "name": "ram" + }, + { + "id": "rds", + "name": "RDS" + }, + { + "id": "rds-data", + "name": "RDS data" + }, + { + "id": "redshift", + "name": "Redshift" + }, + { + "id": "redshift-data", + "name": "Redshift Data" + }, + { + "id": "resource-groups", + "name": "Resource Groups" + }, + { + "id": "resourcegroupstaggingapi", + "name": "Resource Groups Tagging API" + }, + { + "id": "route53", + "name": "Route 53" + }, + { + "id": "route53resolver", + "name": "Route 53 Resolver" + }, + { + "id": "s3", + "name": "S3" + }, + { + "id": "s3control", + "name": "S3 Control" + }, + { + "id": "s3tables", + "name": "S3tables" + }, + { + "id": "sagemaker", + "name": "SageMaker" + }, + { + "id": "sagemaker-runtime", + "name": "SageMaker Runtime" + }, + { + "id": "scheduler", + "name": "scheduler" + }, + { + "id": "secretsmanager", + "name": "Secrets Manager" + }, + { + "id": "serverlessrepo", + "name": "Serverless Application Repository" + }, + { + "id": "servicediscovery", + "name": "Service Discovery" + }, + { + "id": "ses", + "name": "SES" + }, + { + "id": "sesv2", + "name": "SES v2" + }, + { + "id": "shield", + "name": "Shield" + }, + { + "id": "sns", + "name": "SNS" + }, + { + "id": "sqs", + "name": "SQS" + }, + { + "id": "ssm", + "name": "SSM" + }, + { + "id": "sso-admin", + "name": "sso-admin" + }, + { + "id": "states", + "name": "Step Functions" + }, + { + "id": "sts", + "name": "STS" + }, + { + "id": "support", + "name": "Support API" + }, + { + "id": "swf", + "name": "SWF" + }, + { + "id": "textract", + "name": "Textract" + }, + { + "id": "timestream-query", + "name": "Timestream Query" + }, + { + "id": "timestream-write", + "name": "Timestream Write" + }, + { + "id": "transcribe", + "name": "Transcribe" + }, + { + "id": "transfer", + "name": "Transfer" + }, + { + "id": "verifiedpermissions", + "name": "Verifiedpermissions" + }, + { + "id": "wafv2", + "name": "Wafv2" + }, + { + "id": "xray", + "name": "X-Ray" + } + ] +} diff --git a/src/extension.ts b/src/extension.ts index 034927f..2997059 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import appInspectorWebview from "./plugins/app-inspector-webview.ts"; import configureAws from "./plugins/configure-aws.ts"; import logs from "./plugins/logs.ts"; import manage from "./plugins/manage.ts"; +import resourceBrowser from "./plugins/resource-browser.ts"; import setup from "./plugins/setup.ts"; import statusBar from "./plugins/status-bar.ts"; import { PluginManager } from "./plugins.ts"; @@ -23,6 +24,7 @@ const plugins = new PluginManager([ statusBar, logs, appInspectorWebview, + resourceBrowser, ]); export async function activate(context: ExtensionContext) { @@ -49,7 +51,7 @@ export async function activate(context: ExtensionContext) { statusBarItem.show(); const containerStatusTracker = await createContainerStatusTracker( - "localstack-main", + ["localstack-main", "localstack-aws"], outputChannel, timeTracker, ); diff --git a/src/models/focus.ts b/src/models/focus.ts new file mode 100644 index 0000000..451af1e --- /dev/null +++ b/src/models/focus.ts @@ -0,0 +1,72 @@ +/* + * This file contains the models for a "focus" object, which describes a + * specific configuration of which profiles, regions, services, resource types, + * and resources should be shown in the UI. The model is platform-neutral: AWS + * (and future emulators) populate it via their own code under src/platforms/. + */ +import * as z from "zod"; + +const ResourceTypeFocus = z.object({ + id: z.string(), + get arns() { + return z.array(z.string()); + }, +}); + +const ServiceFocus = z.object({ + id: z.string(), + get resourcetypes() { + return z.array(ResourceTypeFocus); + }, +}); + +const RegionFocus = z.object({ + id: z.string(), + get services() { + return z.array(ServiceFocus); + }, +}); + +const ProfileFocus = z.object({ + id: z.string(), + get regions() { + return z.array(RegionFocus); + }, +}); + +export const Focus = z.object({ + version: z.string(), + get profiles() { + return z.array(ProfileFocus); + }, +}); + +export type Focus = z.infer; +export type ProfileFocus = z.infer; +export type RegionFocus = z.infer; +export type ServiceFocus = z.infer; +export type ResourceTypeFocus = z.infer; + +/** + * Build a wildcard focus that shows everything in a single profile/region. + * The service wildcard (`*`) is expanded dynamically by the Resources view. + */ +export function makeWildcardFocus(profileId: string, regionId: string): Focus { + return Focus.parse({ + version: "1.0", + profiles: [ + { + id: profileId, + regions: [ + { + id: regionId, + services: [{ id: "*", resourcetypes: [{ id: "*", arns: ["*"] }] }], + }, + ], + }, + ], + }); +} + +/* Multi-focus merging was removed with the multi-select feature; the LocalStack + * view is single-select, so the Resources view always shows exactly one focus. */ diff --git a/src/platforms/aws/clients/account.ts b/src/platforms/aws/clients/account.ts new file mode 100644 index 0000000..2127f58 --- /dev/null +++ b/src/platforms/aws/clients/account.ts @@ -0,0 +1,55 @@ +import { + AccountClient, + AccountClientConfig, + ListRegionsCommand, + RegionOptStatus, +} from "@aws-sdk/client-account"; +import type { ListRegionsCommandInput } from "@aws-sdk/client-account"; + +import { memoize } from "../../../utils/memoize.ts"; +import AWSConfig from "../models/awsConfig.ts"; + +const cachedGetAccountClient = memoize( + (profile: string) => new AccountClient(AWSConfig.getClientConfig(profile)), +); + +const cachedListRegions = memoize(async (profile: string) => { + const client = cachedGetAccountClient(profile); + + const request: ListRegionsCommandInput = { + RegionOptStatusContains: [ + RegionOptStatus.ENABLED_BY_DEFAULT, + RegionOptStatus.ENABLED, + ], + }; + const RegionNames: string[] = []; + + while (true) { + const response = await client.send(new ListRegionsCommand(request)); + if (response.Regions) { + RegionNames.push( + ...response.Regions.flatMap((region) => + region.RegionName ? [region.RegionName] : [], + ), + ); + } + if (!response.NextToken) { + break; + } + request.NextToken = response.NextToken; + } + return RegionNames; +}); + +/** + * Accessor functions for the AWS "account" service + */ +export const Account = { + /** + * Return the list of AWS regions available for this profile. For example: + * ['ap-southeast-2', 'us-east-1', 'us-west-2'] + */ + listRegions(profile: string): Promise { + return cachedListRegions(profile); + }, +}; diff --git a/src/platforms/aws/clients/cloudformation.ts b/src/platforms/aws/clients/cloudformation.ts new file mode 100644 index 0000000..c040da9 --- /dev/null +++ b/src/platforms/aws/clients/cloudformation.ts @@ -0,0 +1,121 @@ +import { + CloudFormationClient, + DescribeStacksCommand, + ListStackResourcesCommand, + ListStacksCommand, +} from "@aws-sdk/client-cloudformation"; +import type { + ListStacksCommandOutput, + StackResourceSummary, + StackStatus, + StackSummary, +} from "@aws-sdk/client-cloudformation"; + +import { InternalError } from "../../../utils/errors.ts"; +import { memoize } from "../../../utils/memoize.ts"; +import type ARN from "../models/arnModel.ts"; +import AWSConfig from "../models/awsConfig.ts"; + +const cachedGetCloudFormationClient = memoize( + (profile: string, region: string) => + new CloudFormationClient(AWSConfig.getClientConfig(profile, region)), +); + +/** + * Accessor functions for the AWS "cloudformation" service + */ +export const CloudFormation = { + /** + * List the successfully-created stacks of the specified profile — i.e. those + * in a stable, resource-bearing state. Stacks that failed, rolled back from a + * failed create, or are being deleted are omitted (see the status allowlist + * below). If the profile is not valid, reject the promise and let the caller + * behave appropriately. + */ + async listStacks(profile: string, region: string): Promise { + const client = cachedGetCloudFormationClient(profile, region); + + const stacks: StackSummary[] = []; + let nextToken: string | undefined; + + /* + * Only list stacks that have been successfully provisioned and therefore + * have live, inspectable resources. Stacks that never finished creating, + * failed, rolled back from a failed create, or are being deleted are + * excluded: their resources either don't exist or lack the identifiers + * (e.g. PhysicalResourceId) the Resources view relies on, so surfacing + * them only yields empty or broken stack views. The retained states are + * the stable, resource-bearing terminal states (and their transient + * cleanup tails). This will need to be updated if AWS adds new statuses. + */ + const createdStatuses: StackStatus[] = [ + "CREATE_COMPLETE", + "UPDATE_COMPLETE", + "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "UPDATE_ROLLBACK_COMPLETE", + "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", + "IMPORT_COMPLETE", + "IMPORT_ROLLBACK_COMPLETE", + ]; + do { + const command = new ListStacksCommand({ + NextToken: nextToken, + StackStatusFilter: createdStatuses, + }); + const response: ListStacksCommandOutput = await client.send(command); + if (response.StackSummaries) { + stacks.push(...response.StackSummaries); + } + nextToken = response.NextToken; + } while (nextToken); + + return stacks; + }, + + /** + * Describe a specific CloudFormation stack. + */ + async describeStacks(profile: string, region: string, arn: ARN) { + const client = cachedGetCloudFormationClient(profile, region); + + /* + * Note: although there might be many 'deleted' stacks with the same name + * (with a UUID appended to the ARN), there can only be a single active stack + * with a given name. + */ + const command = new DescribeStacksCommand({ StackName: arn.resourceName }); + const response = await client.send(command); + if (response.NextToken) { + throw new InternalError("NextToken not handled for DescribeStacks call"); + } + if (response.Stacks && response.Stacks.length > 0) { + return response.Stacks[0]; + } else { + throw new InternalError(`No stack found with name: ${arn.resourceName}`); + } + }, + + /** + * Invoke the listStackResources API call. + */ + async listStackResources(profile: string, arn: ARN) { + const client = cachedGetCloudFormationClient(profile, arn.region); + + const resources: StackResourceSummary[] = []; + let nextToken: string | undefined; + + do { + const command: ListStackResourcesCommand = new ListStackResourcesCommand({ + StackName: arn.resourceName, + NextToken: nextToken, + }); + const response = await client.send(command); + if (response.StackResourceSummaries) { + resources.push(...response.StackResourceSummaries); + } + nextToken = response.NextToken; + } while (nextToken); + + return resources; + }, +}; diff --git a/src/platforms/aws/clients/iam.ts b/src/platforms/aws/clients/iam.ts new file mode 100644 index 0000000..29c7adf --- /dev/null +++ b/src/platforms/aws/clients/iam.ts @@ -0,0 +1,87 @@ +import { + GetRoleCommand, + IAMClient, + IAMClientConfig, + ListAccountAliasesCommand, + ListRolesCommand, +} from "@aws-sdk/client-iam"; +import type { GetRoleCommandOutput } from "@aws-sdk/client-iam"; + +import { memoize } from "../../../utils/memoize.ts"; +import type ARN from "../models/arnModel.ts"; +import AWSConfig from "../models/awsConfig.ts"; + +const cachedGetIamClient = memoize( + (profile: string) => new IAMClient(AWSConfig.getClientConfig(profile)), +); + +const cachedGetAccountAlias = memoize(async (profile: string) => { + const client = cachedGetIamClient(profile); + const command = new ListAccountAliasesCommand(); + try { + const response = await client.send(command); + if (response.AccountAliases && response.AccountAliases.length > 0) { + return response.AccountAliases[0]; + } else { + return ""; + } + } catch (ex) { + console.error(`Failed to access account aliases for: ${profile}`); + throw ex; + } +}); + +/** + * Accessor functions for the AWS "iam" service + */ +export const IAM = { + /** + * Get the account alias of the specified profile. If the profile is not valid, + * return undefined and let the caller behave appropriately. Note that only + * the first account alias is returned. + */ + getAccountAlias(profile: string): Promise { + return cachedGetAccountAlias(profile); + }, + + /** + * List the IAM Roles in the specified profile. If the profile is not valid, + * reject the promise and let the caller behave appropriately. + */ + async listRoles(profile: string): Promise { + const client = cachedGetIamClient(profile); + + const roles: string[] = []; + let marker: string | undefined; + do { + const command: ListRolesCommand = new ListRolesCommand({ + Marker: marker, + }); + const response = await client.send(command); + if (response.Roles) { + roles.push( + ...response.Roles.flatMap((role) => (role.Arn ? [role.Arn] : [])), + ); + } + marker = response.Marker; + } while (marker); + return roles; + }, + + /** + * Get details about a specific IAM role. + */ + async getRole(profile: string, roleArn: ARN): Promise { + const client = cachedGetIamClient(profile); + + /* Role names can be path-qualified, so we need to extract just the name part */ + let roleName = roleArn.resourceName ?? ""; + const lastSlash = roleName.lastIndexOf("/"); + if (lastSlash !== -1) { + roleName = roleName.substring(lastSlash + 1); + } + + const command = new GetRoleCommand({ RoleName: roleName }); + return await client.send(command); + }, +}; diff --git a/src/platforms/aws/clients/sts.ts b/src/platforms/aws/clients/sts.ts new file mode 100644 index 0000000..2958b39 --- /dev/null +++ b/src/platforms/aws/clients/sts.ts @@ -0,0 +1,41 @@ +import { + GetCallerIdentityCommand, + GetCallerIdentityCommandOutput, + STSClient, + STSClientConfig, +} from "@aws-sdk/client-sts"; + +import { memoize } from "../../../utils/memoize.ts"; +import AWSConfig from "../models/awsConfig.ts"; + +const cachedGetStsClient = memoize((profile: string) => { + return new STSClient(AWSConfig.getClientConfig(profile)); +}); + +const cachedGetCallerIdentity = memoize(async (profile: string) => { + const client = cachedGetStsClient(profile); + const command = new GetCallerIdentityCommand(); + try { + const response = await client.send(command); + if (!response.Account) { + throw new Error("Failed to fetch account ID from profile"); + } + return { account: response.Account }; + } catch (ex) { + console.error(`Failed to access profile: ${profile}`); + throw ex; + } +}); + +/** + * Accessor functions for the AWS "sts" service + */ +export const STS = { + /** + * Get the caller identity of the specified profile. If the profile is not valid, + * reject the promise and let the caller behave appropriately. + */ + getCallerIdentity(profile: string): Promise<{ account: string }> { + return cachedGetCallerIdentity(profile); + }, +}; diff --git a/src/platforms/aws/models/arnModel.ts b/src/platforms/aws/models/arnModel.ts new file mode 100644 index 0000000..05bfb53 --- /dev/null +++ b/src/platforms/aws/models/arnModel.ts @@ -0,0 +1,51 @@ +import { InternalError } from "../../../utils/errors.ts"; + +/** + * Represents an AWS ARN (Amazon Resource Name). This class is used for extracting + * the various fields from the ARN string. + */ +export default class ARN { + readonly partition: string; + readonly service: string; + readonly region: string; + readonly accountId: string; + readonly resourceId: string; + readonly resourceType?: string; + readonly resourceName?: string; + + constructor(public readonly arn: string) { + const arnRegex = + /^arn:(aws[-a-z]*):([a-zA-Z0-9\-.]+):([a-zA-Z0-9\-.]*):([0-9]{12})?(:|\/)(.*)$/; + const match = arnRegex.exec(arn); + if (!match) { + throw new InternalError(`Invalid or unhandled ARN: ${arn}`); + } + + /* required fields */ + this.partition = match[1]; + this.service = match[2]; + this.region = match[3]; + this.accountId = match[4]; + this.resourceId = match[6]; + + /* depending on the service, the resourceId can be dissected further */ + const resourceParts = this.resourceId.split(/[/:]/); + if (resourceParts.length > 1) { + this.resourceType = resourceParts[0]; + if (this.service === "cloudformation") { + this.resourceName = + resourceParts[1]; /* ignore the UUID from the CloudFormation stack ARN */ + } else { + this.resourceName = this.resourceId.substring( + this.resourceType.length + 1, + ); + } + } else { + this.resourceName = this.resourceId; + } + } + + toString(): string { + return this.arn; + } +} diff --git a/src/platforms/aws/models/awsConfig.ts b/src/platforms/aws/models/awsConfig.ts new file mode 100644 index 0000000..1aee929 --- /dev/null +++ b/src/platforms/aws/models/awsConfig.ts @@ -0,0 +1,129 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { parse } from "js-ini"; +import type { IIniObject, IIniObjectSection } from "js-ini"; + +import { UserConfigurationError } from "../../../utils/errors.ts"; + +const DEFAULT_AWS_CONFIG_FILE = path.join(os.homedir(), ".aws", "config"); + +/** Last-resort region when neither the call nor the profile supplies one. */ +const DEFAULT_REGION = "us-east-1"; + +/** Read and parse the AWS config file */ +function readAWSConfigFile(): IIniObject { + try { + const configContent = fs.readFileSync(AWSConfig.AWS_CONFIG_FILE, "utf-8"); + return parse(configContent, { comment: [";", "#"] }); + } catch (error: unknown) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + /* file does not exist, return empty config */ + return {}; + } + if (error instanceof Error) { + /* file exists but is malformed */ + throw new UserConfigurationError(error.message); + } + /* other unknown error */ + throw error; + } +} + +/** Return the configuration section for the specified profile */ +function getSectionForProfile(profile: string): IIniObjectSection | undefined { + const parsedConfig = readAWSConfigFile(); + const section = profile === "default" ? "default" : `profile ${profile}`; + const value = parsedConfig[section]; + /* A section is an object; skip scalars and data sections (string[]). */ + if (typeof value !== "object" || Array.isArray(value)) { + return undefined; + } + return value; +} + +/** + * Accessor functions for reading AWS configuration files and extracting + * profiles and regions. + */ +const AWSConfig = { + /** + * Path to the AWS config file. Exposed as a mutable property so tests can + * repoint it at a fixture file instead of the developer's real config. + */ + AWS_CONFIG_FILE: DEFAULT_AWS_CONFIG_FILE, + + /** + * Return the names of profiles found in the AWS config file. This will include 'default' + * as the name of the default profile. + */ + getProfileNames(): string[] { + try { + const parsedConfig = readAWSConfigFile(); + + const profiles: string[] = []; + for (const section in parsedConfig) { + if (typeof parsedConfig[section] === "object") { + if (section.startsWith("profile ")) { + profiles.push(section.replace("profile ", "")); + } else if (section === "default") { + profiles.push("default"); + } + } + } + return profiles; + } catch (err) { + if (err instanceof UserConfigurationError) { + throw err; + } + /* file not found or other error */ + return []; + } + }, + + /** + * Return the default region name. First, check the AWS_REGION environment variable, + * and then check the .aws/config file. + */ + getRegionForProfile(profile: string): string | undefined { + /* The environment variable takes precedence */ + if (process.env.AWS_REGION) { + return process.env.AWS_REGION; + } + + const region = getSectionForProfile(profile)?.region; + return typeof region === "string" ? region : undefined; + }, + + /** + * Return a configuration object suitable for passing to an AWS SDK client constructor. This + * is necessary to ensure the endpoint is set correctly when using a non-standard endpoint + * (such as LocalStack). + * + * When no region is supplied, fall back to the profile's default region and + * then a built-in default. Some resources have region-less ARNs (e.g. an S3 + * bucket `arn:aws:s3:::name`); describing one passes the empty ARN region + * here, and the SDK rejects an empty region with "Region is missing". Listing + * is unaffected because it always passes the focus region. + */ + getClientConfig(profile: string, region?: string): object { + /* use endpoint from profile, if it's defined */ + const endpoint = AWSConfig.getEndpointForProfile(profile); + const resolvedRegion = + region || AWSConfig.getRegionForProfile(profile) || DEFAULT_REGION; + return { profile, region: resolvedRegion, endpoint }; + }, + + /** + * Return the custom `endpoint_url` configured for a profile, or `undefined` + * when the profile targets real AWS (no override). A custom endpoint is the + * signal that a profile points at LocalStack (or another local emulator). + */ + getEndpointForProfile(profile: string): string | undefined { + const endpointUrl = getSectionForProfile(profile)?.endpoint_url; + return typeof endpointUrl === "string" ? endpointUrl : undefined; + }, +}; + +export default AWSConfig; diff --git a/src/platforms/aws/models/cfnStackModel.ts b/src/platforms/aws/models/cfnStackModel.ts new file mode 100644 index 0000000..02993e3 --- /dev/null +++ b/src/platforms/aws/models/cfnStackModel.ts @@ -0,0 +1,208 @@ +import { Stack, StackResource } from "@aws-sdk/client-cloudformation"; +import type { StackResourceSummary } from "@aws-sdk/client-cloudformation"; +import type { LogOutputChannel } from "vscode"; + +import { Focus } from "../../../models/focus.ts"; +import type { ServiceFocus } from "../../../models/focus.ts"; +import { InternalError } from "../../../utils/errors.ts"; +import { CloudFormation } from "../clients/cloudformation.ts"; +import { ProviderFactory } from "../services/providerFactory.ts"; +import { mapLabelToServiceId } from "../services/serviceManifest.ts"; +import type { ServiceResourceArnTuple } from "../services/serviceProvider.ts"; + +import type ARN from "./arnModel.ts"; + +/** + * Model representing a CloudFormation Stack, with the ability to return + * the equivalent Focus model for the stack's resources. This allows the + * Focus-based UI to display the resources that belong to a CloudFormation stack. + */ +export default class CfnStackModel { + private partition: string; + private region: string; + private accountId: string; + + /** + * Constructor for the CfnStackModel class. + * @param profile The profile associated with the CloudFormation stack. + * @param arn The ARN of the CloudFormation stack. + * @param log Optional output channel used to report stack resources that + * could not be mapped into the Focus model (see `convertToServicesList`). + */ + constructor( + public profile: string, + public arn: ARN, + private readonly log?: LogOutputChannel, + ) { + if (arn.service !== "cloudformation" || arn.resourceType !== "stack") { + throw new InternalError( + `Invalid CloudFormation Stack ARN: ${arn.toString()}`, + ); + } + this.partition = arn.partition; + this.region = arn.region; + this.accountId = arn.accountId; + } + + /** + * Query the resources belonging to this CloudFormation stack, then return an + * equivalent Focus model showing only the relevant resources. + */ + public async toFocusModel(): Promise { + const stackResources = await CloudFormation.listStackResources( + this.profile, + this.arn, + ); + const servicesList = this.convertToServicesList(stackResources); + + const focus: Focus = Focus.parse({ + version: "1.0", + profiles: [ + { + id: this.profile, + regions: [ + { + id: this.region, + services: servicesList, + }, + ], + }, + ], + }); + return focus; + } + + /** + * Convert the full list of CloudFormation stack resources into the Focus services + * format. This is done in multiple steps: + * 1. Convert each CloudFormation resource (e.g. "AWS::SQS::Queue") into a tuple of + * (serviceName, resourceTypeName, resourceArn), such as ("sqs", "queue", "arn:aws:sqs:..."). + * Conversion is per-resource and fault-tolerant: a resource we can't recognize or + * map to an ARN is skipped (with a warning logged) rather than aborting the whole + * stack, so the stack still shows every resource we *can* represent. + * 2. Group the tuples by service and resource type, to create the hierarchical structure + * required by Focus. + * 3. Traverse our existing ordered list of services and resource types (within services) + * to build the final services list for the Focus. Using this consistent ordering + * ensures a consistent display on the UI. + */ + private convertToServicesList( + stackResources: StackResourceSummary[], + ): ServiceFocus[] { + /* + * Convert each CloudFormation resource into a (service, resourceType, arn) + * tuple. Any resource that can't be converted — an unsupported service or + * resource type, or a summary missing the fields we rely on — is skipped + * with a warning instead of throwing, which would otherwise prevent the + * entire stack from loading. + */ + const tuples: ServiceResourceArnTuple[] = []; + for (const resource of stackResources) { + try { + tuples.push(this.convertToTuple(resource)); + } catch (error) { + this.log?.warn( + `[cfn-stack] Skipping unrecognized resource ` + + `${resource.LogicalResourceId} (${resource.ResourceType}): ${String(error)}`, + ); + } + } + + /* group the ARNs by service and resource type, so we end up with a hierarchical map */ + const servicesMap = new Map>(); + tuples.forEach(({ serviceId, resourceType, arn }) => { + let resourceMap = servicesMap.get(serviceId); + if (!resourceMap) { + resourceMap = new Map(); + servicesMap.set(serviceId, resourceMap); + } + let arns = resourceMap.get(resourceType); + if (!arns) { + arns = []; + resourceMap.set(resourceType, arns); + } + arns.push(arn); + }); + + /* + * Traverse our ordered list of services, and resource types to build the final structure. + * This ensures the services and resource types appear in a consistent order on the UI, + * regardless of the order they were discovered in the CloudFormation stack. + */ + const serviceFocusList: ServiceFocus[] = []; + + ProviderFactory.getSupportedServices().forEach((serviceProvider) => { + const serviceId = serviceProvider.getId(); + const resourceMap = servicesMap.get(serviceId); + + /* If the cloudformation stack has resources for this service, add them to the list */ + if (resourceMap) { + const resourceTypes: { id: string; arns: string[] }[] = []; + + /* Traverse the resource types in order */ + const orderedResourceTypes = serviceProvider.getResourceTypes(); + orderedResourceTypes.forEach((resourceType) => { + const arns = resourceMap.get(resourceType); + if (arns) { + resourceTypes.push({ id: resourceType, arns }); + } + }); + + if (resourceTypes.length > 0) { + serviceFocusList.push({ + id: serviceId, + resourcetypes: resourceTypes, + }); + } + } + }); + + return serviceFocusList; + } + + /** + * Convert a single CloudFormation stack resource into a tuple of + * (serviceName, resourceTypeName, resourceArn) + */ + private convertToTuple( + stackResourceSummary: StackResourceSummary, + ): ServiceResourceArnTuple { + const cfnType = stackResourceSummary.ResourceType; + if (!cfnType) { + throw new InternalError( + "CloudFormation resource is missing a ResourceType", + ); + } + const [aws, service] = cfnType.split("::"); // e.g. ["AWS", "SQS", "Queue"] + + /* We only support CloudFormation resources that start with AWS:: */ + if (aws !== "AWS") { + throw new InternalError( + `Unsupported CloudFormation resource: ${cfnType}`, + ); + } + + /* Map the CloudFormation namespace (e.g. "StepFunctions") to a manifest + * service id (e.g. "states"), then look up its provider. A service with no + * registered provider — not yet curated — is treated as unrepresentable: + * the caller skips and logs it rather than aborting the whole stack. */ + const serviceId = mapLabelToServiceId(service); + const serviceHandler = ProviderFactory.tryGetProviderForService(serviceId); + if (!serviceHandler) { + throw new InternalError( + `No registered provider for service: ${serviceId}`, + ); + } + + /* + * For the specific CloudFormation resource type (e.g. AWS::SQS::Queue), compute the + * resourceType (e.g. "function") and the resourceName name portion of the ARN (e.g. "function:my-lambda-function") + */ + const { resourceType, resourceName } = + serviceHandler.getArnResourceNameForCloudFormationResource( + stackResourceSummary, + ); + const arn = `arn:${this.partition}:${serviceId}:${this.region}:${this.accountId}:${resourceName}`; + return { serviceId, resourceType, arn }; + } +} diff --git a/src/platforms/aws/models/metamodelFocus.ts b/src/platforms/aws/models/metamodelFocus.ts new file mode 100644 index 0000000..5729ddf --- /dev/null +++ b/src/platforms/aws/models/metamodelFocus.ts @@ -0,0 +1,189 @@ +/* + * Translates the running emulator's pods-state metamodel into a platform-neutral + * Focus for the LocalStack Instances "View: All Resources" selector. + * + * Metamodel shape (account -> Service label -> region -> apiOperation -> response): + * { "000000000000": { "S3": { "us-east-1": { "listBuckets": { ... } } } } } + * + * This is the transpose of the Focus shape (profile -> region -> service -> ...). + * We name only the services/regions actually present (mapped to a registered + * provider) and leave resource-type/ARN selectors as wildcards so the existing + * SDK providers list the live resources on drill-down. + */ +import type { LogOutputChannel } from "vscode"; + +import { Focus } from "../../../models/focus.ts"; +import { ProviderFactory } from "../services/providerFactory.ts"; +import { mapLabelToServiceId } from "../services/serviceManifest.ts"; + +const METAMODEL_PATH = "/_localstack/pods/state/metamodel"; + +/** v1 assumes a single account; other accounts are deliberately omitted. */ +const DEFAULT_ACCOUNT = "000000000000"; + +/** The LocalStack profile that backs the instance section. */ +const LOCALSTACK_PROFILE = "localstack"; + +type MetamodelPayload = Record; + +/** + * Parse the metamodel payload leniently: the live endpoint can emit raw control + * characters inside string values, which strict JSON.parse rejects. Exported for + * testing. + */ +export function parseMetamodel(text: string): MetamodelPayload { + const sanitized = text.replace( + // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping invalid control chars that break strict JSON.parse + /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, + "", + ); + return JSON.parse(sanitized) as MetamodelPayload; +} + +/** + * Pure translation of a parsed metamodel payload into a Focus. `resourceTypes` + * maps each supported provider id to its resource-type ids; `operationMaps` maps + * each provider id to its metamodel-operation → resource-type map. Services whose + * mapped id is absent from `resourceTypes` are omitted (and logged). The + * global-service mirror region ("") is skipped. Exported for testing. + * + * For each present service/region the focus names only the resource types whose + * metamodel list-operation key is present (so a service with one deployed type + * does not render its other types). When a present operation maps to no known + * resource type — or the provider declares no operation map — the service falls + * back to its full resource-type set (and the gap is logged), so a missing + * mapping never hides resources that exist. + */ +export function metamodelToFocus( + payload: MetamodelPayload, + resourceTypes: Map, + operationMaps: Map>, + log?: LogOutputChannel, +): Focus { + const account = payload[DEFAULT_ACCOUNT] as MetamodelPayload | undefined; + if (!account) { + /* No state for the default account: an empty (but valid) focus. */ + return Focus.parse({ + version: "1.0", + profiles: [{ id: LOCALSTACK_PROFILE, regions: [] }], + }); + } + + /* region id -> provider id -> set of present resource-type ids */ + const regionServices = new Map>>(); + const dropped = new Set(); + const fellBack = new Set(); + + for (const [serviceLabel, regions] of Object.entries(account)) { + const providerId = mapLabelToServiceId(serviceLabel); + const allTypes = resourceTypes.get(providerId); + if (!allTypes) { + dropped.add(serviceLabel); + continue; + } + const opMap = operationMaps.get(providerId) ?? new Map(); + + for (const [region, opsByName] of Object.entries( + regions as MetamodelPayload, + )) { + /* Skip the global-service mirror ("") to avoid an empty region node. */ + if (region === "") { + continue; + } + + /* The metamodel records one list-operation key per present resource + * type. Map those to resource types; fall back to the full set when an + * operation is unmapped (or none map), so resources are never hidden. */ + const present = new Set(); + let unmappedOp = false; + for (const op of Object.keys((opsByName as MetamodelPayload) ?? {})) { + const resourceType = opMap.get(op); + if (resourceType) { + present.add(resourceType); + } else { + unmappedOp = true; + } + } + const types = unmappedOp || present.size === 0 ? allTypes : [...present]; + if (unmappedOp) { + fellBack.add(serviceLabel); + } + + let svcMap = regionServices.get(region); + if (!svcMap) { + svcMap = new Map>(); + regionServices.set(region, svcMap); + } + const existing = svcMap.get(providerId); + if (existing) { + for (const t of types) { + existing.add(t); + } + } else { + svcMap.set(providerId, new Set(types)); + } + } + } + + if (dropped.size > 0) { + log?.info( + `[metamodel] Omitted services with no provider: ${[...dropped] + .sort() + .join(", ")}`, + ); + } + if (fellBack.size > 0) { + log?.info( + `[metamodel] Listed all resource types (unmapped operation) for: ${[ + ...fellBack, + ] + .sort() + .join(", ")}`, + ); + } + + const focusRegions = [...regionServices.entries()].map( + ([regionId, svcMap]) => ({ + id: regionId, + services: [...svcMap.entries()].map(([serviceId, typeIds]) => ({ + id: serviceId, + resourcetypes: [...typeIds].map((rt) => ({ + id: rt, + arns: ["*"], + })), + })), + }), + ); + + return Focus.parse({ + version: "1.0", + profiles: [{ id: LOCALSTACK_PROFILE, regions: focusRegions }], + }); +} + +/** + * Fetch and translate the emulator metamodel into a Focus. Throws if the + * emulator is unreachable or the payload cannot be parsed; the caller surfaces + * that as a non-fatal error state. + */ +export async function computeMetamodelFocus( + endpointUrl: string, + log?: LogOutputChannel, +): Promise { + const url = `${endpointUrl.replace(/\/$/, "")}${METAMODEL_PATH}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Metamodel request failed: HTTP ${response.status}`); + } + const payload = parseMetamodel(await response.text()); + + const services = ProviderFactory.getSupportedServices(); + const resourceTypes = new Map( + services.map((p) => [p.getId(), p.getResourceTypes()]), + ); + const operationMaps = new Map>( + services.map((p) => [p.getId(), p.getMetamodelOperationMap()]), + ); + + return metamodelToFocus(payload, resourceTypes, operationMaps, log); +} diff --git a/src/platforms/aws/models/regionModel.ts b/src/platforms/aws/models/regionModel.ts new file mode 100644 index 0000000..bff7300 --- /dev/null +++ b/src/platforms/aws/models/regionModel.ts @@ -0,0 +1,65 @@ +/* + * To save an API call (or many), we hard-code these region names. + * AWS will add more over time and we can update this list as needed. + */ +const REGION_NAMES: { [key: string]: string } = { + "af-south-1": "Africa (Cape Town)", + "ap-east-1": "Asia Pacific (Hong Kong)", + "ap-east-2": "Asia Pacific (Taipei)", + "ap-south-1": "Asia Pacific (Mumbai)", + "ap-south-2": "Asia Pacific (Hyderabad)", + "ap-northeast-1": "Asia Pacific (Tokyo)", + "ap-northeast-2": "Asia Pacific (Seoul)", + "ap-northeast-3": "Asia Pacific (Osaka)", + "ap-southeast-1": "Asia Pacific (Singapore)", + "ap-southeast-2": "Asia Pacific (Sydney)", + "ap-southeast-3": "Asia Pacific (Jakarta)", + "ap-southeast-4": "Asia Pacific (Melbourne)", + "ap-southeast-5": "Asia Pacific (Malaysia)", + "ap-southeast-6": "Asia Pacific (New Zealand)", + "ap-southeast-7": "Asia Pacific (Thailand)", + "ca-central-1": "Canada (Central)", + "ca-west-1": "Canada West (Calgary)", + "cn-north-1": "China (Beijing)", + "cn-northwest-1": "China (Ningxia)", + "eu-central-1": "Europe (Frankfurt)", + "eu-central-2": "Europe (Zurich)", + "eu-north-1": "Europe (Stockholm)", + "eu-south-1": "Europe (Milan)", + "eu-south-2": "Europe (Spain)", + "eu-west-1": "Europe (Ireland)", + "eu-west-2": "Europe (London)", + "eu-west-3": "Europe (Paris)", + "il-central-1": "Israel (Tel Aviv)", + "me-south-1": "Middle East (Bahrain)", + "me-central-1": "Middle East (UAE)", + "mx-central-1": "Mexico (Central)", + "sa-east-1": "South America (São Paulo)", + "us-east-1": "US East (N. Virginia)", + "us-east-2": "US East (Ohio)", + "us-gov-east-1": "AWS GovCloud (US-East)", + "us-gov-west-1": "AWS GovCloud (US-West)", + "us-west-1": "US West (N. California)", + "us-west-2": "US West (Oregon)", +}; + +/** + * Get the long name of an AWS region. + * @param region The short name of the region (e.g., "us-east-1"). + * @returns The long name of the region (e.g., "US East (N. Virginia)"), or the + * region code itself when it is not in the table. The running emulator can + * report any region in its metamodel (including partitions we have not + * enumerated), so an unknown region must degrade to its code rather than throw + * and break the whole "All Resources" view. + */ +export function getRegionLongName(region: string): string { + return REGION_NAMES[region] ?? region; +} + +/** + * Return all known AWS region codes (e.g. "us-east-1"), for offering the user + * a list of regions to add without an API call. + */ +export function getAllRegionCodes(): string[] { + return Object.keys(REGION_NAMES); +} diff --git a/src/platforms/aws/services/declarative/engine.ts b/src/platforms/aws/services/declarative/engine.ts new file mode 100644 index 0000000..29c0cfc --- /dev/null +++ b/src/platforms/aws/services/declarative/engine.ts @@ -0,0 +1,263 @@ +/* + * The engine that turns a declarative `ServiceDefinition` into a working + * `ServiceProvider`. It adapts the definition's data + per-type closures to the + * provider interface (list resource types, list resources, describe a resource + * by walking field paths, map CloudFormation types), constructing the service's + * SDK client through `AWSConfig.getClientConfig` so the same definition works + * against the LocalStack emulator and real AWS. + */ +import type { StackResourceSummary } from "@aws-sdk/client-cloudformation"; + +import { InternalError } from "../../../../utils/errors.ts"; +import { memoize } from "../../../../utils/memoize.ts"; +import type ARN from "../../models/arnModel.ts"; +import AWSConfig from "../../models/awsConfig.ts"; +import { FieldType, ServiceProvider } from "../serviceProvider.ts"; + +import type { + FieldSpec, + ResourceTypeDefinition, + ServiceDefinition, +} from "./types.ts"; + +/** + * Read a value out of a detail object by a dotted/bracketed path, + * e.g. `"Configuration.State"` or `"Tags[0].Value"`. Returns `undefined` if any + * segment is missing. + */ +export function getByPath(obj: unknown, path: string): unknown { + const parts = path + .replace(/\[(\d+)\]/g, ".$1") + .split(".") + .filter((part) => part.length > 0); + let current: unknown = obj; + for (const part of parts) { + if (current === null || current === undefined) { + return undefined; + } + current = (current as Record)[part]; + } + return current; +} + +/** Stringify an unknown value safely (objects as JSON, never `[object Object]`). */ +function safeToString(value: unknown): string { + if (value === null || value === undefined) { + return ""; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "object") { + return JSON.stringify(value); + } + if ( + typeof value === "number" || + typeof value === "boolean" || + typeof value === "bigint" + ) { + return String(value); + } + /* symbol / function: not meaningfully displayable */ + return ""; +} + +/** One rendered Resource Details row. */ +type DetailField = { field: string; value: string; type: FieldType }; + +/** + * Render one detail spec against the detail object into zero or more rows. A + * scalar spec yields a single row; a `list` spec yields a header row followed by + * one indented row per array element (none if the array is missing/empty). + */ +export function renderDetailField( + spec: FieldSpec, + detailObject: unknown, +): DetailField[] { + if ("kind" in spec) { + const itemType = spec.itemType ?? FieldType.NAME; + const array = getByPath(detailObject, spec.path); + const items = Array.isArray(array) ? array : []; + return [ + { field: spec.label, value: "", type: FieldType.NAME }, + ...items.map((item) => ({ + field: ` ${formatValue(getByPath(item, spec.itemLabel), FieldType.NAME)}`, + value: formatValue(getByPath(item, spec.itemValue), itemType), + type: itemType, + })), + ]; + } + return [ + { + field: spec.label, + value: formatValue(getByPath(detailObject, spec.path), spec.type), + type: spec.type, + }, + ]; +} + +/** Render a raw value as the display string for its `FieldType`. */ +export function formatValue(value: unknown, type: FieldType): string { + if (value === null || value === undefined) { + return ""; + } + switch (type) { + case FieldType.DATE: + if (value instanceof Date) { + return value.toISOString(); + } + /* epoch seconds or millis */ + if (typeof value === "number") { + const millis = value < 1e12 ? value * 1000 : value; + return new Date(millis).toISOString(); + } + return safeToString(value); + case FieldType.JSON: + return typeof value === "string" ? value : JSON.stringify(value, null, 2); + default: + return safeToString(value); + } +} + +/** + * A `ServiceProvider` backed by a declarative `ServiceDefinition`. One instance + * per service; the SDK client is created lazily and memoized per profile/region. + */ +export class DeclarativeServiceProvider extends ServiceProvider { + protected resourceTypes: Record; + + private readonly getClient: (profile: string, region: string) => TClient; + + constructor(private readonly definition: ServiceDefinition) { + super(); + this.resourceTypes = Object.fromEntries( + Object.entries(definition.resourceTypes).map(([id, def]) => [ + id, + [def.singular, def.plural] as [string, string], + ]), + ); + this.getClient = memoize((profile: string, region: string) => + definition.client(AWSConfig.getClientConfig(profile, region)), + ); + } + + getId(): string { + return this.definition.id; + } + + getName(): string { + return this.definition.name; + } + + override getMetamodelOperationMap(): Map { + const map = new Map(); + for (const [resourceType, def] of Object.entries( + this.definition.resourceTypes, + )) { + if (def.metamodelOp) { + map.set(def.metamodelOp, resourceType); + } + } + return map; + } + + async getResourceArns( + profile: string, + region: string, + resourceType: string, + ): Promise { + const def = this.definition.resourceTypes[resourceType]; + if (!def) { + throw new InternalError(`Unknown resource type: ${resourceType}`); + } + const client = this.getClient(profile, region); + const items = await def.list(client, { profile, region }); + return items.map((item) => def.id(item, { profile, region })); + } + + async describeResource( + profile: string, + arn: ARN, + ): Promise<{ field: string; value: string; type: FieldType }[]> { + const def = this.resolveResourceType(arn); + const client = this.getClient(profile, arn.region); + + let detailObject: unknown; + if (def.describe) { + detailObject = await def.describe(client, arn, { + profile, + region: arn.region, + }); + } else { + /* "self": list and find the matching item by identifier. */ + const ctx = { profile, region: arn.region }; + const items = await def.list(client, ctx); + detailObject = items.find((item) => def.id(item, ctx) === arn.toString()); + if (detailObject === undefined) { + throw new InternalError(`Resource not found: ${arn.toString()}`); + } + } + + return def.detail.flatMap((spec) => renderDetailField(spec, detailObject)); + } + + getArnResourceNameForCloudFormationResource( + stackResourceSummary: StackResourceSummary, + ): { resourceType: string; resourceName: string } { + const cfnType = stackResourceSummary.ResourceType; + const entry = Object.entries(this.definition.resourceTypes).find( + ([, def]) => def.cfn === cfnType, + ); + if (!entry) { + throw new Error( + `Unsupported resource type for ${this.definition.name}: ${cfnType}`, + ); + } + const [resourceType, def] = entry; + const resourceName = def.cfnResourceName + ? def.cfnResourceName(stackResourceSummary) + : stackResourceSummary.PhysicalResourceId; + if (!resourceName) { + throw new Error( + `Missing identifier for CloudFormation resource ${cfnType}`, + ); + } + return { resourceType, resourceName }; + } + + /** + * Resolve which resource type an ARN refers to. With a single type, that type + * is used; otherwise the ARN's resource-segment token is matched against each + * type's `arnType` (defaulting to its id), case-insensitively. + */ + private resolveResourceType( + arn: ARN, + // biome-ignore lint/suspicious/noExplicitAny: item type is erased across the record + ): ResourceTypeDefinition { + const entries = Object.entries(this.definition.resourceTypes) as [ + string, + ResourceTypeDefinition, + ][]; + if (entries.length === 1) { + return entries[0][1]; + } + /* An explicit predicate wins (for types sharing an ARN resource token). */ + const byPredicate = entries.find(([, def]) => { + const predicate = def.matchArn; + return typeof predicate === "function" && predicate(arn); + }); + if (byPredicate) { + return byPredicate[1]; + } + const arnToken = arn.resourceType?.toLowerCase(); + const match = entries.find( + ([id, def]) => (def.arnType ?? id).toLowerCase() === arnToken, + ); + if (!match) { + throw new InternalError( + `Cannot resolve resource type for ARN: ${arn.toString()}`, + ); + } + return match[1]; + } +} diff --git a/src/platforms/aws/services/declarative/types.ts b/src/platforms/aws/services/declarative/types.ts new file mode 100644 index 0000000..1cdc085 --- /dev/null +++ b/src/platforms/aws/services/declarative/types.ts @@ -0,0 +1,172 @@ +/* + * Declarative service-provider format. + * + * A curated provider can be authored as data — a `ServiceDefinition` — instead + * of a hand-written `ServiceProvider` subclass. A shared engine + * (`DeclarativeServiceProvider`) executes the definition against the AWS SDK. + * This is NOT a generic provider: nothing works without a per-service + * definition that declares its resource types, how to list them, how to + * identify each resource, how to map its CloudFormation type, and which detail + * fields to show. It is curation expressed as data. + */ +import type { StackResourceSummary } from "@aws-sdk/client-cloudformation"; + +import type ARN from "../../models/arnModel.ts"; +import type { FieldType } from "../serviceProvider.ts"; + +/** Context handed to a resource type's `list` call. */ +export type ListContext = { + profile: string; + region: string; +}; + +/** Context handed to a resource type's `describe` call. */ +export type DescribeContext = { + profile: string; + region: string; +}; + +/** + * One scalar field shown in the Resource Details view. `path` is a + * dotted/bracketed path walked over the detail object (e.g. + * `"Configuration.State"`, `"Tags[0].Value"`). This is the unit the build-time + * field generator emits and humans hand-edit. + */ +export type ScalarFieldSpec = { + label: string; + path: string; + type: FieldType; +}; + +/** + * A variable-length section: a header row followed by one row per element of an + * array on the detail object. Use this for resource details whose count is not + * known at authoring time, e.g. a DynamoDB table's key schema or a + * CloudFormation stack's parameters/outputs. `path` locates the array; for each + * element, `itemLabel`/`itemValue` are paths walked over that element to derive + * the (indented) field label and its value. + */ +export type ListFieldSpec = { + kind: "list"; + /** Section header label, rendered as a row with an empty value. */ + label: string; + /** Path to the array on the detail object. */ + path: string; + /** Path within each element → the field label (rendered indented). */ + itemLabel: string; + /** Path within each element → the field value. */ + itemValue: string; + /** Value type for each element's row. Defaults to `FieldType.NAME`. */ + itemType?: FieldType; +}; + +/** + * One entry in a resource type's `detail` list: either a single scalar field or + * a variable-length array-expanded section (`kind: "list"`). + */ +export type FieldSpec = ScalarFieldSpec | ListFieldSpec; + +/** + * Declarative definition of a single resource type within a service. + * + * @typeParam TClient The service's AWS SDK client type. + * @typeParam TItem The shape of one item returned by `list`. + */ +export type ResourceTypeDefinition = { + /** Human-facing singular name, e.g. "Bucket". */ + singular: string; + /** Human-facing plural name, e.g. "Buckets". */ + plural: string; + /** + * CloudFormation resource type name, e.g. "AWS::S3::Bucket". Omit when the + * resource type has no CloudFormation representation. + */ + cfn?: string; + /** + * The token used to recognize this resource type within an ARN's resource + * segment (case-insensitive), for services with more than one type. Defaults + * to the resource type's id (its key in `resourceTypes`). + */ + arnType?: string; + /** + * Optional predicate to recognize whether an ARN belongs to this resource + * type, for services whose types share an ARN resource token (e.g. a Kinesis + * stream vs. its consumer, both under `stream/`). Checked before `arnType`. + */ + matchArn?: (identifier: ARN) => boolean; + /** + * The metamodel API-operation name that signals this resource type's presence + * (the camelCase list operation, e.g. `"describeParameters"`, `"listKeys"`). + * Used to narrow the LocalStack "View: All Resources" focus to the resource + * types actually deployed. Annotate every type of a multi-type service so the + * narrowing is precise; omit for single-type services (the metamodel focus + * falls back to the sole type). When omitted, the type cannot be matched and + * the service falls back to its full type set if any present op is unmapped. + */ + metamodelOp?: string; + /** List the live resources for a profile/region. Returns raw API items. */ + list: (client: TClient, ctx: ListContext) => Promise; + /** + * The resource's identifier — its ARN where the API returns one, else a + * synthesized ARN. The list context is provided so a resource whose list + * response omits an ARN can build one from the region (account may be left + * empty), keeping identifiers parseable by the details view. + */ + id: (item: TItem, ctx: ListContext) => string; + /** + * Fetch the object the detail fields are read from for a single resource. + * Omit to reuse the matching `list` item ("self"): the engine lists, then + * selects the item whose `id` matches the requested ARN. + */ + describe?: ( + client: TClient, + identifier: ARN, + ctx: DescribeContext, + ) => Promise; + /** The ordered, typed subset of fields shown in Resource Details. */ + detail: FieldSpec[]; + /** + * Derive the ARN resource-name portion for a CloudFormation resource of this + * type. For multi-type services this must encode the type so the ARN can be + * resolved back to this resource type (e.g. `"statemachine:Name"`). Defaults + * to the resource's `PhysicalResourceId`. + */ + cfnResourceName?: (summary: StackResourceSummary) => string | undefined; +}; + +/** Declarative definition of a whole service. */ +export type ServiceDefinition = { + /** AWS service code (manifest id), e.g. "s3". */ + id: string; + /** Display name, e.g. "S3". */ + name: string; + /** Construct the service's SDK client from an AWSConfig client config. */ + client: (config: object) => TClient; + /** + * Resource types keyed by their id (the value used in focus/ARN paths). Each + * entry may have its own item type, so the item generic is erased here; use + * `defineResourceType` to author one with inference. + */ + // biome-ignore lint/suspicious/noExplicitAny: per-type item shapes differ; erased at the record level + resourceTypes: Record>; +}; + +/** + * Author a service definition. Identity at runtime; exists for inference and to + * make definition files read declaratively. + */ +export function defineService( + definition: ServiceDefinition, +): ServiceDefinition { + return definition; +} + +/** + * Author a single resource type with full inference of its client and item + * types (so `list`/`id`/`describe` are type-checked against the API shapes). + */ +export function defineResourceType( + definition: ResourceTypeDefinition, +): ResourceTypeDefinition { + return definition; +} diff --git a/src/platforms/aws/services/definitions/apigateway.ts b/src/platforms/aws/services/definitions/apigateway.ts new file mode 100644 index 0000000..01dd380 --- /dev/null +++ b/src/platforms/aws/services/definitions/apigateway.ts @@ -0,0 +1,273 @@ +import { + APIGatewayClient, + GetApiKeyCommand, + GetApiKeysCommand, + GetAuthorizersCommand, + GetRestApiCommand, + GetRestApisCommand, + GetStagesCommand, + GetUsagePlanCommand, + GetUsagePlansCommand, +} from "@aws-sdk/client-api-gateway"; +import type { + ApiKey, + Authorizer, + RestApi, + Stage, + UsagePlan, +} from "@aws-sdk/client-api-gateway"; + +import type ARN from "../../models/arnModel.ts"; +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/* Stages and authorizers are scoped to a REST API but their list items omit the + * api id; augment them so a path-style ARN can be synthesized. */ +type StageWithApi = Stage & { restApiId: string }; +type AuthorizerWithApi = Authorizer & { restApiId: string }; + +/** + * API Gateway (v1 / REST). REST APIs, stages, API keys, usage plans, and + * authorizers. API Gateway uses path-style ARNs + * (`arn:aws:apigateway:::/restapis/`) with no account, synthesized + * here from the region; the path segment distinguishes the types and detail is + * read from the list item. + */ +export const apiGatewayDefinition = defineService({ + id: "apigateway", + name: "API Gateway", + client: (config) => new APIGatewayClient(config), + resourceTypes: { + restapi: { + singular: "REST API", + plural: "REST APIs", + metamodelOp: "getRestApis", + cfn: "AWS::ApiGateway::RestApi", + /* CloudFormation's PhysicalResourceId is the REST API id; re-encode it as + * the `/restapis/` path the live `id` uses so a stack resource + * resolves to this type and `describe` can read it back. */ + cfnResourceName: (summary) => + summary.PhysicalResourceId + ? `/restapis/${summary.PhysicalResourceId}` + : undefined, + matchArn: (identifier) => + identifier.arn.includes("/restapis/") && + !identifier.arn.includes("/stages/") && + !identifier.arn.includes("/authorizers/"), + list: async (client): Promise => { + const apis: RestApi[] = []; + let position: string | undefined; + do { + const out = await client.send(new GetRestApisCommand({ position })); + apis.push(...(out.items ?? [])); + position = out.position; + } while (position); + return apis; + }, + id: (api: RestApi, ctx) => + `arn:aws:apigateway:${ctx.region}::/restapis/${api.id}`, + /* Fetch directly by id (works for both live and CloudFormation ARNs) + * rather than re-listing every API. */ + describe: (client, identifier) => + client.send( + new GetRestApiCommand({ restApiId: pathId(identifier, "restapis") }), + ), + detail: [ + { label: "Name", path: "name", type: FieldType.NAME }, + { label: "ID", path: "id", type: FieldType.NAME }, + { + label: "Description", + path: "description", + type: FieldType.SHORT_TEXT, + }, + { label: "Version", path: "version", type: FieldType.NAME }, + { label: "API Key Source", path: "apiKeySource", type: FieldType.NAME }, + { label: "Created Date", path: "createdDate", type: FieldType.DATE }, + ], + }, + stage: { + singular: "Stage", + plural: "Stages", + metamodelOp: "getStages", + /* No `cfn` mapping: CloudFormation's PhysicalResourceId for a stage is the + * stage name alone, without the REST API id needed to fetch it. Live + * stages (whose ARN carries the api id) still resolve; CloudFormation + * stages are skipped in stack views. */ + matchArn: (identifier) => identifier.arn.includes("/stages/"), + list: async (client): Promise => { + const apiIds = await listRestApiIds(client); + const stages: StageWithApi[] = []; + for (const restApiId of apiIds) { + const out = await client.send(new GetStagesCommand({ restApiId })); + stages.push( + ...(out.item ?? []).map((stage) => ({ ...stage, restApiId })), + ); + } + return stages; + }, + id: (stage: StageWithApi, ctx) => + `arn:aws:apigateway:${ctx.region}::/restapis/${stage.restApiId}/stages/${stage.stageName}`, + detail: [ + { label: "Stage Name", path: "stageName", type: FieldType.NAME }, + { label: "REST API ID", path: "restApiId", type: FieldType.NAME }, + { label: "Deployment ID", path: "deploymentId", type: FieldType.NAME }, + { + label: "Description", + path: "description", + type: FieldType.SHORT_TEXT, + }, + { label: "Created Date", path: "createdDate", type: FieldType.DATE }, + ], + }, + apikey: { + singular: "API Key", + plural: "API Keys", + metamodelOp: "getApiKeys", + cfn: "AWS::ApiGateway::ApiKey", + /* CloudFormation's PhysicalResourceId is the API key id; re-encode it as + * the `/apikeys/` path the live `id` uses. */ + cfnResourceName: (summary) => + summary.PhysicalResourceId + ? `/apikeys/${summary.PhysicalResourceId}` + : undefined, + matchArn: (identifier) => identifier.arn.includes("/apikeys/"), + list: async (client): Promise => { + const keys: ApiKey[] = []; + let position: string | undefined; + do { + const out = await client.send(new GetApiKeysCommand({ position })); + keys.push(...(out.items ?? [])); + position = out.position; + } while (position); + return keys; + }, + id: (key: ApiKey, ctx) => + `arn:aws:apigateway:${ctx.region}::/apikeys/${key.id}`, + /* Fetch by id (no `includeValue`, so the key secret is never read). */ + describe: (client, identifier) => + client.send( + new GetApiKeyCommand({ apiKey: pathId(identifier, "apikeys") }), + ), + detail: [ + { label: "Name", path: "name", type: FieldType.NAME }, + { label: "ID", path: "id", type: FieldType.NAME }, + { label: "Enabled", path: "enabled", type: FieldType.NAME }, + { + label: "Description", + path: "description", + type: FieldType.SHORT_TEXT, + }, + { label: "Created Date", path: "createdDate", type: FieldType.DATE }, + ], + }, + usageplan: { + singular: "Usage Plan", + plural: "Usage Plans", + metamodelOp: "getUsagePlans", + cfn: "AWS::ApiGateway::UsagePlan", + /* CloudFormation's PhysicalResourceId is the usage plan id; re-encode it + * as the `/usageplans/` path the live `id` uses. */ + cfnResourceName: (summary) => + summary.PhysicalResourceId + ? `/usageplans/${summary.PhysicalResourceId}` + : undefined, + matchArn: (identifier) => identifier.arn.includes("/usageplans/"), + list: async (client): Promise => { + const plans: UsagePlan[] = []; + let position: string | undefined; + do { + const out = await client.send(new GetUsagePlansCommand({ position })); + plans.push(...(out.items ?? [])); + position = out.position; + } while (position); + return plans; + }, + id: (plan: UsagePlan, ctx) => + `arn:aws:apigateway:${ctx.region}::/usageplans/${plan.id}`, + /* Fetch directly by id rather than re-listing every usage plan. */ + describe: (client, identifier) => + client.send( + new GetUsagePlanCommand({ + usagePlanId: pathId(identifier, "usageplans"), + }), + ), + detail: [ + { label: "Name", path: "name", type: FieldType.NAME }, + { label: "ID", path: "id", type: FieldType.NAME }, + { + label: "Description", + path: "description", + type: FieldType.SHORT_TEXT, + }, + { label: "Product Code", path: "productCode", type: FieldType.NAME }, + ], + }, + authorizer: { + singular: "Authorizer", + plural: "Authorizers", + metamodelOp: "getAuthorizers", + /* No `cfn` mapping: CloudFormation's PhysicalResourceId for an authorizer + * is the authorizer id alone, without the REST API id needed to fetch it. + * Live authorizers still resolve; CloudFormation authorizers are skipped. */ + matchArn: (identifier) => identifier.arn.includes("/authorizers/"), + list: async (client): Promise => { + const apiIds = await listRestApiIds(client); + const authorizers: AuthorizerWithApi[] = []; + for (const restApiId of apiIds) { + const out = await client.send( + new GetAuthorizersCommand({ restApiId }), + ); + authorizers.push( + ...(out.items ?? []).map((authorizer) => ({ + ...authorizer, + restApiId, + })), + ); + } + return authorizers; + }, + id: (authorizer: AuthorizerWithApi, ctx) => + `arn:aws:apigateway:${ctx.region}::/restapis/${authorizer.restApiId}/authorizers/${authorizer.id}`, + detail: [ + { label: "Name", path: "name", type: FieldType.NAME }, + { label: "ID", path: "id", type: FieldType.NAME }, + { label: "Type", path: "type", type: FieldType.NAME }, + { label: "REST API ID", path: "restApiId", type: FieldType.NAME }, + { + label: "Auth Type", + path: "authType", + type: FieldType.NAME, + }, + ], + }, + }, +}); + +/** + * Extract the id following a path collection in an API Gateway ARN, e.g. the + * `` in `/restapis/` or `/apikeys/`. Works for both the live + * (account-less) ARN and the CloudFormation-synthesized one, since both encode + * the same `/collection/` path in the resource portion. Returns `""` when + * the collection segment is absent. + */ +function pathId(identifier: ARN, collection: string): string { + const parts = identifier.resourceId.split("/").filter(Boolean); + const index = parts.indexOf(collection); + return index >= 0 ? (parts[index + 1] ?? "") : ""; +} + +/** Enumerate every REST API id (for resource types scoped to an API). */ +async function listRestApiIds(client: APIGatewayClient): Promise { + const ids: string[] = []; + let position: string | undefined; + do { + const out = await client.send(new GetRestApisCommand({ position })); + ids.push( + ...(out.items ?? []) + .map((api) => api.id) + .filter((id): id is string => Boolean(id)), + ); + position = out.position; + } while (position); + return ids; +} diff --git a/src/platforms/aws/services/definitions/cloudformation.ts b/src/platforms/aws/services/definitions/cloudformation.ts new file mode 100644 index 0000000..a007240 --- /dev/null +++ b/src/platforms/aws/services/definitions/cloudformation.ts @@ -0,0 +1,131 @@ +import { + CloudFormationClient, + DescribeStacksCommand, + ListStacksCommand, +} from "@aws-sdk/client-cloudformation"; +import type { StackStatus, StackSummary } from "@aws-sdk/client-cloudformation"; + +import { InternalError } from "../../../../utils/errors.ts"; +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/* + * Only list stacks in a stable, resource-bearing state. Stacks that never + * finished creating, failed, rolled back from a failed create, or are being + * deleted are excluded: their resources either don't exist or lack the + * identifiers the Resources view relies on. Update this if AWS adds statuses. + */ +const CREATED_STATUSES: StackStatus[] = [ + "CREATE_COMPLETE", + "UPDATE_COMPLETE", + "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "UPDATE_ROLLBACK_COMPLETE", + "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", + "IMPORT_COMPLETE", + "IMPORT_ROLLBACK_COMPLETE", +]; + +/** + * CloudFormation. Stacks only, filtered to the stable created/updated states + * and sorted by stack id for a consistent display order. Parameters and outputs + * are variable-length, rendered via `list` detail specs; the boolean + * protection/rollback flags and the capabilities array are flattened into + * display strings by `describe`. No CloudFormation self-mapping (a stack is not + * itself a stack resource). + */ +export const cloudFormationDefinition = defineService({ + id: "cloudformation", + name: "CloudFormation", + client: (config) => new CloudFormationClient(config), + resourceTypes: { + stack: { + singular: "Stack", + plural: "Stacks", + list: async (client): Promise => { + const stacks: StackSummary[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListStacksCommand({ + NextToken: nextToken, + StackStatusFilter: CREATED_STATUSES, + }), + ); + stacks.push(...(out.StackSummaries ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return stacks + .filter((stack) => stack.StackId) + .sort((a, b) => (a.StackId ?? "").localeCompare(b.StackId ?? "")); + }, + id: (stack: StackSummary) => stack.StackId ?? "", + describe: async (client, identifier) => { + const out = await client.send( + new DescribeStacksCommand({ StackName: identifier.resourceName }), + ); + const stack = out.Stacks?.[0]; + if (!stack) { + throw new InternalError( + `No stack found with name: ${identifier.resourceName}`, + ); + } + return { + ...stack, + TerminationProtectionText: stack.EnableTerminationProtection + ? "Enabled" + : "Disabled", + RollbackText: stack.DisableRollback ? "Disabled" : "Enabled", + CapabilitiesText: stack.Capabilities?.length + ? stack.Capabilities.join(", ") + : "None", + }; + }, + detail: [ + { label: "Stack Name", path: "StackName", type: FieldType.NAME }, + { label: "Stack Status", path: "StackStatus", type: FieldType.NAME }, + { + label: "Description", + path: "Description", + type: FieldType.SHORT_TEXT, + }, + { label: "Change Set ARN", path: "ChangeSetId", type: FieldType.ARN }, + { label: "Creation Time", path: "CreationTime", type: FieldType.DATE }, + { + label: "Last Updated Time", + path: "LastUpdatedTime", + type: FieldType.DATE, + }, + { + label: "Termination Protection", + path: "TerminationProtectionText", + type: FieldType.NAME, + }, + { label: "Rollback", path: "RollbackText", type: FieldType.NAME }, + { + label: "Capabilities", + path: "CapabilitiesText", + type: FieldType.SHORT_TEXT, + }, + { + label: "Drift Status", + path: "DriftInformation.StackDriftStatus", + type: FieldType.NAME, + }, + { + kind: "list", + label: "Parameters", + path: "Parameters", + itemLabel: "ParameterKey", + itemValue: "ParameterValue", + }, + { + kind: "list", + label: "Outputs", + path: "Outputs", + itemLabel: "OutputKey", + itemValue: "OutputValue", + }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/cognito-idp.ts b/src/platforms/aws/services/definitions/cognito-idp.ts new file mode 100644 index 0000000..0b723fb --- /dev/null +++ b/src/platforms/aws/services/definitions/cognito-idp.ts @@ -0,0 +1,189 @@ +import { + CognitoIdentityProviderClient, + DescribeUserPoolCommand, + ListGroupsCommand, + ListUserPoolClientsCommand, + ListUserPoolsCommand, +} from "@aws-sdk/client-cognito-identity-provider"; +import type { + GroupType, + UserPoolClientDescription, + UserPoolDescriptionType, +} from "@aws-sdk/client-cognito-identity-provider"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * Cognito User Pools (the `cognito-idp` service code). User pools plus their + * app clients and groups, enumerated across pools and presented flat. None of + * these list responses carry ARNs, so account-less ARNs are synthesized from + * the region; client/group detail is read from the list item. + * + * Note: identity pools belong to the separate `cognito-identity` service (a + * different SDK client) and are intentionally not included here. + */ +export const cognitoIdpDefinition = + defineService({ + id: "cognito-idp", + name: "Cognito IDP", + client: (config) => new CognitoIdentityProviderClient(config), + resourceTypes: { + userpool: { + singular: "User Pool", + plural: "User Pools", + cfn: "AWS::Cognito::UserPool", + /* CloudFormation's PhysicalResourceId for a user pool is the pool id; + * re-encode it as the `userpool/` token the live `id` uses so a + * stack resource resolves to this type and `describe` can read it. */ + cfnResourceName: (summary) => + summary.PhysicalResourceId + ? `userpool/${summary.PhysicalResourceId}` + : undefined, + matchArn: (identifier) => identifier.arn.includes(":userpool/"), + list: async (client): Promise => { + const pools: UserPoolDescriptionType[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListUserPoolsCommand({ + MaxResults: 60, + NextToken: nextToken, + }), + ); + pools.push(...(out.UserPools ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return pools; + }, + id: (pool: UserPoolDescriptionType, ctx) => + `arn:aws:cognito-idp:${ctx.region}::userpool/${pool.Id}`, + describe: (client, identifier) => + client.send( + new DescribeUserPoolCommand({ + UserPoolId: identifier.resourceName, + }), + ), + detail: [ + { label: "Name", path: "UserPool.Name", type: FieldType.NAME }, + { label: "ID", path: "UserPool.Id", type: FieldType.NAME }, + { label: "Status", path: "UserPool.Status", type: FieldType.NAME }, + { + label: "MFA Configuration", + path: "UserPool.MfaConfiguration", + type: FieldType.NAME, + }, + { + label: "Estimated Users", + path: "UserPool.EstimatedNumberOfUsers", + type: FieldType.NUMBER, + }, + { + label: "Creation Date", + path: "UserPool.CreationDate", + type: FieldType.DATE, + }, + ], + }, + userpoolclient: { + singular: "User Pool Client", + plural: "User Pool Clients", + /* No `cfn` mapping: CloudFormation's PhysicalResourceId for an app + * client is the client id alone, without the user pool id that every + * Cognito API needs to describe it — so a stack-origin client can't be + * resolved. Live resources (which carry the pool id in their ARN) still + * work; CloudFormation app clients are skipped in stack views. */ + matchArn: (identifier) => identifier.arn.includes(":userpoolclient/"), + list: async (client): Promise => { + const poolIds = await listUserPoolIds(client); + const clients: UserPoolClientDescription[] = []; + for (const userPoolId of poolIds) { + let nextToken: string | undefined; + do { + const out = await client.send( + new ListUserPoolClientsCommand({ + UserPoolId: userPoolId, + MaxResults: 60, + NextToken: nextToken, + }), + ); + clients.push(...(out.UserPoolClients ?? [])); + nextToken = out.NextToken; + } while (nextToken); + } + return clients; + }, + id: (appClient: UserPoolClientDescription, ctx) => + `arn:aws:cognito-idp:${ctx.region}::userpoolclient/${appClient.UserPoolId}/${appClient.ClientId}`, + detail: [ + { label: "Client Name", path: "ClientName", type: FieldType.NAME }, + { label: "Client ID", path: "ClientId", type: FieldType.NAME }, + { label: "User Pool ID", path: "UserPoolId", type: FieldType.NAME }, + ], + }, + userpoolgroup: { + singular: "User Pool Group", + plural: "User Pool Groups", + /* No `cfn` mapping: CloudFormation's PhysicalResourceId for a group is + * the group name alone, without the user pool id needed to describe it. + * Live resources still resolve; CloudFormation groups are skipped. */ + matchArn: (identifier) => identifier.arn.includes(":userpoolgroup/"), + list: async (client): Promise => { + const poolIds = await listUserPoolIds(client); + const groups: GroupType[] = []; + for (const userPoolId of poolIds) { + let nextToken: string | undefined; + do { + const out = await client.send( + new ListGroupsCommand({ + UserPoolId: userPoolId, + NextToken: nextToken, + }), + ); + groups.push(...(out.Groups ?? [])); + nextToken = out.NextToken; + } while (nextToken); + } + return groups; + }, + id: (group: GroupType, ctx) => + `arn:aws:cognito-idp:${ctx.region}::userpoolgroup/${group.UserPoolId}/${group.GroupName}`, + detail: [ + { label: "Group Name", path: "GroupName", type: FieldType.NAME }, + { label: "User Pool ID", path: "UserPoolId", type: FieldType.NAME }, + { + label: "Description", + path: "Description", + type: FieldType.SHORT_TEXT, + }, + { label: "Precedence", path: "Precedence", type: FieldType.NUMBER }, + { label: "Role ARN", path: "RoleArn", type: FieldType.ARN }, + { + label: "Creation Date", + path: "CreationDate", + type: FieldType.DATE, + }, + ], + }, + }, + }); + +/** Enumerate every user pool id (for resource types scoped to a pool). */ +async function listUserPoolIds( + client: CognitoIdentityProviderClient, +): Promise { + const ids: string[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListUserPoolsCommand({ MaxResults: 60, NextToken: nextToken }), + ); + ids.push( + ...(out.UserPools ?? []) + .map((pool) => pool.Id) + .filter((id): id is string => Boolean(id)), + ); + nextToken = out.NextToken; + } while (nextToken); + return ids; +} diff --git a/src/platforms/aws/services/definitions/dynamodb.ts b/src/platforms/aws/services/definitions/dynamodb.ts new file mode 100644 index 0000000..c9f7a7f --- /dev/null +++ b/src/platforms/aws/services/definitions/dynamodb.ts @@ -0,0 +1,116 @@ +import { + DescribeTableCommand, + DynamoDBClient, + ListTablesCommand, +} from "@aws-sdk/client-dynamodb"; +import type { TableDescription } from "@aws-sdk/client-dynamodb"; + +import { InternalError } from "../../../../utils/errors.ts"; +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** A listed table paired with its derived ARN (the list API returns names only). */ +type DynamoTable = { TableName: string; TableArn: string }; + +/** + * DynamoDB. Tables only. `ListTables` returns names without ARNs, so `list` + * derives the ARN prefix from the first table's description and applies it to + * every name. The table's attribute definitions and key schema are + * variable-length, rendered via `list` detail specs. + */ +export const dynamoDbDefinition = defineService({ + id: "dynamodb", + name: "DynamoDB", + client: (config) => new DynamoDBClient(config), + resourceTypes: { + table: { + singular: "Table", + plural: "Tables", + list: async (client): Promise => { + const names: string[] = []; + let start: string | undefined; + do { + const out = await client.send( + new ListTablesCommand({ ExclusiveStartTableName: start }), + ); + names.push(...(out.TableNames ?? [])); + start = out.LastEvaluatedTableName; + } while (start); + if (names.length === 0) { + return []; + } + /* Names only — derive the ARN prefix from the first table. */ + const first = await client.send( + new DescribeTableCommand({ TableName: names[0] }), + ); + const firstArn = first.Table?.TableArn; + if (!firstArn) { + throw new InternalError( + `Failed to describe DynamoDB table: ${names[0]}`, + ); + } + const prefix = firstArn.slice(0, firstArn.lastIndexOf("/") + 1); + return names.map((name) => ({ + TableName: name, + TableArn: prefix + name, + })); + }, + id: (table: DynamoTable) => table.TableArn, + describe: async (client, identifier): Promise => { + const out = await client.send( + new DescribeTableCommand({ TableName: identifier.resourceName }), + ); + if (!out.Table) { + throw new InternalError( + `Failed to describe DynamoDB table: ${identifier.resourceName}`, + ); + } + return out.Table; + }, + detail: [ + { label: "Name", path: "TableName", type: FieldType.NAME }, + { label: "Table Status", path: "TableStatus", type: FieldType.NAME }, + { label: "Item Count", path: "ItemCount", type: FieldType.NUMBER }, + { + label: "Table Size (bytes)", + path: "TableSizeBytes", + type: FieldType.NUMBER, + }, + { + label: "Creation Date", + path: "CreationDateTime", + type: FieldType.DATE, + }, + { + label: "Billing Mode", + path: "BillingModeSummary.BillingMode", + type: FieldType.NAME, + }, + { + label: "Provisioned Read Capacity Units", + path: "ProvisionedThroughput.ReadCapacityUnits", + type: FieldType.NUMBER, + }, + { + label: "Provisioned Write Capacity Units", + path: "ProvisionedThroughput.WriteCapacityUnits", + type: FieldType.NUMBER, + }, + { + kind: "list", + label: "Attributes", + path: "AttributeDefinitions", + itemLabel: "AttributeName", + itemValue: "AttributeType", + }, + { + kind: "list", + label: "Key Schema", + path: "KeySchema", + itemLabel: "AttributeName", + itemValue: "KeyType", + }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/ecr.ts b/src/platforms/aws/services/definitions/ecr.ts new file mode 100644 index 0000000..ad1627a --- /dev/null +++ b/src/platforms/aws/services/definitions/ecr.ts @@ -0,0 +1,55 @@ +import { DescribeRepositoriesCommand, ECRClient } from "@aws-sdk/client-ecr"; +import type { Repository } from "@aws-sdk/client-ecr"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** ECR (Elastic Container Registry). Repositories are the browsable type. */ +export const ecrDefinition = defineService({ + id: "ecr", + name: "ECR", + client: (config) => new ECRClient(config), + resourceTypes: { + repository: { + singular: "Repository", + plural: "Repositories", + cfn: "AWS::ECR::Repository", + list: async (client): Promise => { + const repositories: Repository[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new DescribeRepositoriesCommand({ nextToken }), + ); + repositories.push(...(out.repositories ?? [])); + nextToken = out.nextToken; + } while (nextToken); + return repositories; + }, + id: (repo: Repository) => repo.repositoryArn ?? repo.repositoryName ?? "", + describe: async (client, identifier) => { + const out = await client.send( + new DescribeRepositoriesCommand({ + repositoryNames: [identifier.resourceName ?? ""], + }), + ); + return out.repositories?.[0]; + }, + detail: [ + { + label: "Repository Name", + path: "repositoryName", + type: FieldType.NAME, + }, + { label: "ARN", path: "repositoryArn", type: FieldType.ARN }, + { label: "URI", path: "repositoryUri", type: FieldType.SHORT_TEXT }, + { + label: "Tag Mutability", + path: "imageTagMutability", + type: FieldType.NAME, + }, + { label: "Created At", path: "createdAt", type: FieldType.DATE }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/events.ts b/src/platforms/aws/services/definitions/events.ts new file mode 100644 index 0000000..baffea6 --- /dev/null +++ b/src/platforms/aws/services/definitions/events.ts @@ -0,0 +1,221 @@ +import { + DescribeArchiveCommand, + EventBridgeClient, + ListApiDestinationsCommand, + ListArchivesCommand, + ListConnectionsCommand, + ListEventBusesCommand, + ListRulesCommand, +} from "@aws-sdk/client-eventbridge"; +import type { + ApiDestination, + Archive, + Connection, + EventBus, + Rule, +} from "@aws-sdk/client-eventbridge"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * EventBridge. Event buses, rules (enumerated across every bus), API + * destinations, connections, and archives. Most detail is read from the list + * item; archives have no ARN in their list response, so an account-less ARN is + * synthesized from the region to keep them addressable. + */ +export const eventsDefinition = defineService({ + id: "events", + name: "EventBridge", + client: (config) => new EventBridgeClient(config), + resourceTypes: { + eventbus: { + singular: "Event Bus", + plural: "Event Buses", + metamodelOp: "listEventBuses", + cfn: "AWS::Events::EventBus", + matchArn: (identifier) => identifier.arn.includes(":event-bus/"), + list: async (client): Promise => { + const buses: EventBus[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListEventBusesCommand({ NextToken: nextToken }), + ); + buses.push(...(out.EventBuses ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return buses; + }, + id: (bus: EventBus) => bus.Arn ?? bus.Name ?? "", + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { label: "ARN", path: "Arn", type: FieldType.ARN }, + { label: "Policy", path: "Policy", type: FieldType.JSON }, + ], + }, + rule: { + singular: "Rule", + plural: "Rules", + metamodelOp: "listRules", + cfn: "AWS::Events::Rule", + matchArn: (identifier) => identifier.arn.includes(":rule/"), + list: async (client): Promise => { + /* Rules belong to an event bus; enumerate buses, then their rules. */ + const busNames: string[] = []; + let busToken: string | undefined; + do { + const out = await client.send( + new ListEventBusesCommand({ NextToken: busToken }), + ); + busNames.push( + ...(out.EventBuses ?? []) + .map((bus) => bus.Name) + .filter((name): name is string => Boolean(name)), + ); + busToken = out.NextToken; + } while (busToken); + + const rules: Rule[] = []; + for (const busName of busNames) { + let nextToken: string | undefined; + do { + const out = await client.send( + new ListRulesCommand({ + EventBusName: busName, + NextToken: nextToken, + }), + ); + rules.push(...(out.Rules ?? [])); + nextToken = out.NextToken; + } while (nextToken); + } + return rules; + }, + id: (rule: Rule) => rule.Arn ?? rule.Name ?? "", + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { label: "ARN", path: "Arn", type: FieldType.ARN }, + { label: "State", path: "State", type: FieldType.NAME }, + { + label: "Description", + path: "Description", + type: FieldType.SHORT_TEXT, + }, + { + label: "Schedule", + path: "ScheduleExpression", + type: FieldType.SHORT_TEXT, + }, + { label: "Event Bus", path: "EventBusName", type: FieldType.NAME }, + ], + }, + apidestination: { + singular: "API Destination", + plural: "API Destinations", + metamodelOp: "listApiDestinations", + cfn: "AWS::Events::ApiDestination", + matchArn: (identifier) => identifier.arn.includes(":api-destination/"), + list: async (client): Promise => { + const destinations: ApiDestination[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListApiDestinationsCommand({ NextToken: nextToken }), + ); + destinations.push(...(out.ApiDestinations ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return destinations; + }, + id: (destination: ApiDestination) => + destination.ApiDestinationArn ?? destination.Name ?? "", + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { label: "ARN", path: "ApiDestinationArn", type: FieldType.ARN }, + { label: "State", path: "ApiDestinationState", type: FieldType.NAME }, + { + label: "Endpoint", + path: "InvocationEndpoint", + type: FieldType.SHORT_TEXT, + }, + { label: "HTTP Method", path: "HttpMethod", type: FieldType.NAME }, + { label: "Creation Time", path: "CreationTime", type: FieldType.DATE }, + ], + }, + connection: { + singular: "Connection", + plural: "Connections", + metamodelOp: "listConnections", + cfn: "AWS::Events::Connection", + matchArn: (identifier) => identifier.arn.includes(":connection/"), + list: async (client): Promise => { + const connections: Connection[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListConnectionsCommand({ NextToken: nextToken }), + ); + connections.push(...(out.Connections ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return connections; + }, + id: (connection: Connection) => + connection.ConnectionArn ?? connection.Name ?? "", + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { label: "ARN", path: "ConnectionArn", type: FieldType.ARN }, + { label: "State", path: "ConnectionState", type: FieldType.NAME }, + { + label: "Authorization", + path: "AuthorizationType", + type: FieldType.NAME, + }, + { label: "Creation Time", path: "CreationTime", type: FieldType.DATE }, + ], + }, + archive: { + singular: "Archive", + plural: "Archives", + metamodelOp: "listArchives", + cfn: "AWS::Events::Archive", + matchArn: (identifier) => identifier.arn.includes(":archive/"), + list: async (client): Promise => { + const archives: Archive[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListArchivesCommand({ NextToken: nextToken }), + ); + archives.push(...(out.Archives ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return archives; + }, + /* ListArchives omits an ARN; synthesize an account-less one from region. */ + id: (archive: Archive, ctx) => + `arn:aws:events:${ctx.region}::archive/${archive.ArchiveName}`, + describe: (client, identifier) => + client.send( + new DescribeArchiveCommand({ ArchiveName: identifier.resourceName }), + ), + detail: [ + { label: "Name", path: "ArchiveName", type: FieldType.NAME }, + { label: "State", path: "State", type: FieldType.NAME }, + { + label: "Event Source ARN", + path: "EventSourceArn", + type: FieldType.ARN, + }, + { + label: "Retention (days)", + path: "RetentionDays", + type: FieldType.NUMBER, + }, + { label: "Event Count", path: "EventCount", type: FieldType.NUMBER }, + { label: "Creation Time", path: "CreationTime", type: FieldType.DATE }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/iam.ts b/src/platforms/aws/services/definitions/iam.ts new file mode 100644 index 0000000..2f7e32e --- /dev/null +++ b/src/platforms/aws/services/definitions/iam.ts @@ -0,0 +1,89 @@ +import { + GetRoleCommand, + IAMClient, + ListRolesCommand, +} from "@aws-sdk/client-iam"; +import type { Role } from "@aws-sdk/client-iam"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * IAM. A global service (its ARNs carry no region), exposing roles. The role + * name in a role ARN can be path-qualified (`role/path/.../name`), so `describe` + * extracts the trailing name segment for `GetRole`, and the URL-encoded + * `AssumeRolePolicyDocument` is decoded for display. + */ +export const iamDefinition = defineService({ + id: "iam", + name: "IAM", + client: (config) => new IAMClient(config), + resourceTypes: { + role: { + singular: "Role", + plural: "Roles", + cfn: "AWS::IAM::Role", + cfnResourceName: (summary) => + summary.PhysicalResourceId + ? `role/${summary.PhysicalResourceId}` + : undefined, + list: async (client): Promise => { + const roles: Role[] = []; + let marker: string | undefined; + do { + const out = await client.send( + new ListRolesCommand({ Marker: marker }), + ); + roles.push(...(out.Roles ?? [])); + marker = out.Marker; + } while (marker); + return roles; + }, + id: (role: Role) => role.Arn ?? "", + describe: async (client, identifier) => { + /* Role names can be path-qualified; GetRole takes just the name. */ + const resourceName = identifier.resourceName ?? ""; + const roleName = resourceName.includes("/") + ? resourceName.slice(resourceName.lastIndexOf("/") + 1) + : resourceName; + const out = await client.send( + new GetRoleCommand({ RoleName: roleName }), + ); + const role = out.Role; + return { + ...role, + /* AssumeRolePolicyDocument is URL-encoded JSON; decode for display. */ + AssumeRolePolicyDocument: role?.AssumeRolePolicyDocument + ? decodeURIComponent(role.AssumeRolePolicyDocument) + : undefined, + }; + }, + detail: [ + { label: "Role Name", path: "RoleName", type: FieldType.NAME }, + { label: "Role ID", path: "RoleId", type: FieldType.NAME }, + { label: "Path", path: "Path", type: FieldType.SHORT_TEXT }, + { + label: "Description", + path: "Description", + type: FieldType.SHORT_TEXT, + }, + { + label: "Max Session Duration", + path: "MaxSessionDuration", + type: FieldType.NUMBER, + }, + { label: "Created", path: "CreateDate", type: FieldType.DATE }, + { + label: "Last Used", + path: "RoleLastUsed.LastUsedDate", + type: FieldType.DATE, + }, + { + label: "Assume Role Policy", + path: "AssumeRolePolicyDocument", + type: FieldType.JSON, + }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/index.ts b/src/platforms/aws/services/definitions/index.ts new file mode 100644 index 0000000..6b44d5f --- /dev/null +++ b/src/platforms/aws/services/definitions/index.ts @@ -0,0 +1,49 @@ +/* + * Registry of declarative service definitions. Each entry is executed by the + * `DeclarativeServiceProvider` engine and registered through `ProviderFactory` + * exactly like an imperative provider. Add a service by authoring a definition + * (see `defineService`) and listing it here. + * + * The item generic of each definition is erased here (`any`) because every + * service has its own client and item shapes; the engine treats them uniformly. + */ +import type { ServiceDefinition } from "../declarative/types.ts"; + +import { apiGatewayDefinition } from "./apigateway.ts"; +import { cloudFormationDefinition } from "./cloudformation.ts"; +import { cognitoIdpDefinition } from "./cognito-idp.ts"; +import { dynamoDbDefinition } from "./dynamodb.ts"; +import { ecrDefinition } from "./ecr.ts"; +import { eventsDefinition } from "./events.ts"; +import { iamDefinition } from "./iam.ts"; +import { kinesisDefinition } from "./kinesis.ts"; +import { kmsDefinition } from "./kms.ts"; +import { lambdaDefinition } from "./lambda.ts"; +import { logsDefinition } from "./logs.ts"; +import { s3Definition } from "./s3.ts"; +import { secretsManagerDefinition } from "./secretsmanager.ts"; +import { snsDefinition } from "./sns.ts"; +import { sqsDefinition } from "./sqs.ts"; +import { ssmDefinition } from "./ssm.ts"; +import { statesDefinition } from "./states.ts"; + +export const serviceDefinitions: ServiceDefinition[] = [ + s3Definition, + apiGatewayDefinition, + ssmDefinition, + secretsManagerDefinition, + kinesisDefinition, + logsDefinition, + eventsDefinition, + kmsDefinition, + cognitoIdpDefinition, + ecrDefinition, + cloudFormationDefinition, + dynamoDbDefinition, + iamDefinition, + lambdaDefinition, + snsDefinition, + sqsDefinition, + statesDefinition, +]; diff --git a/src/platforms/aws/services/definitions/kinesis.ts b/src/platforms/aws/services/definitions/kinesis.ts new file mode 100644 index 0000000..2b53576 --- /dev/null +++ b/src/platforms/aws/services/definitions/kinesis.ts @@ -0,0 +1,155 @@ +import { + DescribeStreamConsumerCommand, + DescribeStreamSummaryCommand, + KinesisClient, + ListStreamConsumersCommand, + ListStreamsCommand, +} from "@aws-sdk/client-kinesis"; +import type { Consumer, StreamSummary } from "@aws-sdk/client-kinesis"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * Kinesis Data Streams. Streams plus their registered (enhanced fan-out) + * consumers. Consumers are listed region-wide by iterating every stream; their + * ARNs are nested under the stream ARN, so a predicate distinguishes the two + * types (both share the `stream/` resource token). + */ +export const kinesisDefinition = defineService({ + id: "kinesis", + name: "Kinesis", + client: (config) => new KinesisClient(config), + resourceTypes: { + stream: { + singular: "Stream", + plural: "Streams", + metamodelOp: "listStreams", + cfn: "AWS::Kinesis::Stream", + matchArn: (identifier) => !identifier.arn.includes("/consumer/"), + list: async (client): Promise => { + const summaries: StreamSummary[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListStreamsCommand({ NextToken: nextToken }), + ); + summaries.push(...(out.StreamSummaries ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return summaries; + }, + id: (stream: StreamSummary) => stream.StreamARN ?? "", + describe: (client, identifier) => + client.send( + new DescribeStreamSummaryCommand({ + StreamName: identifier.resourceName, + }), + ), + detail: [ + { + label: "Name", + path: "StreamDescriptionSummary.StreamName", + type: FieldType.NAME, + }, + { + label: "ARN", + path: "StreamDescriptionSummary.StreamARN", + type: FieldType.ARN, + }, + { + label: "Status", + path: "StreamDescriptionSummary.StreamStatus", + type: FieldType.NAME, + }, + { + label: "Retention (hours)", + path: "StreamDescriptionSummary.RetentionPeriodHours", + type: FieldType.NUMBER, + }, + { + label: "Open Shards", + path: "StreamDescriptionSummary.OpenShardCount", + type: FieldType.NUMBER, + }, + { + label: "Creation Time", + path: "StreamDescriptionSummary.StreamCreationTimestamp", + type: FieldType.DATE, + }, + ], + }, + streamconsumer: { + singular: "Stream Consumer", + plural: "Stream Consumers", + metamodelOp: "listStreamConsumers", + cfn: "AWS::Kinesis::StreamConsumer", + matchArn: (identifier) => identifier.arn.includes("/consumer/"), + list: async (client): Promise => { + /* Consumers are scoped to a stream; enumerate streams, then their + * consumers, and present the result as a flat, region-wide list. */ + const streamArns: string[] = []; + let streamToken: string | undefined; + do { + const out = await client.send( + new ListStreamsCommand({ NextToken: streamToken }), + ); + streamArns.push( + ...(out.StreamSummaries ?? []) + .map((summary) => summary.StreamARN) + .filter((arn): arn is string => Boolean(arn)), + ); + streamToken = out.NextToken; + } while (streamToken); + + const consumers: Consumer[] = []; + for (const streamArn of streamArns) { + let nextToken: string | undefined; + do { + const out = await client.send( + new ListStreamConsumersCommand({ + StreamARN: streamArn, + NextToken: nextToken, + }), + ); + consumers.push(...(out.Consumers ?? [])); + nextToken = out.NextToken; + } while (nextToken); + } + return consumers; + }, + id: (consumer: Consumer) => consumer.ConsumerARN ?? "", + describe: (client, identifier) => + client.send( + new DescribeStreamConsumerCommand({ ConsumerARN: identifier.arn }), + ), + detail: [ + { + label: "Name", + path: "ConsumerDescription.ConsumerName", + type: FieldType.NAME, + }, + { + label: "ARN", + path: "ConsumerDescription.ConsumerARN", + type: FieldType.ARN, + }, + { + label: "Status", + path: "ConsumerDescription.ConsumerStatus", + type: FieldType.NAME, + }, + { + label: "Stream ARN", + path: "ConsumerDescription.StreamARN", + type: FieldType.ARN, + }, + { + label: "Creation Time", + path: "ConsumerDescription.ConsumerCreationTimestamp", + type: FieldType.DATE, + }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/kms.ts b/src/platforms/aws/services/definitions/kms.ts new file mode 100644 index 0000000..d124355 --- /dev/null +++ b/src/platforms/aws/services/definitions/kms.ts @@ -0,0 +1,96 @@ +import { + DescribeKeyCommand, + KMSClient, + ListAliasesCommand, + ListKeysCommand, +} from "@aws-sdk/client-kms"; +import type { AliasListEntry, KeyListEntry } from "@aws-sdk/client-kms"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * KMS (Key Management Service). Customer keys and aliases. Key detail comes + * from DescribeKey (nested under `KeyMetadata`); alias detail is taken from the + * ListAliases item. + */ +export const kmsDefinition = defineService({ + id: "kms", + name: "KMS", + client: (config) => new KMSClient(config), + resourceTypes: { + key: { + singular: "Key", + plural: "Keys", + metamodelOp: "listKeys", + cfn: "AWS::KMS::Key", + list: async (client): Promise => { + const keys: KeyListEntry[] = []; + let marker: string | undefined; + do { + const out = await client.send( + new ListKeysCommand({ Marker: marker }), + ); + keys.push(...(out.Keys ?? [])); + marker = out.Truncated ? out.NextMarker : undefined; + } while (marker); + return keys; + }, + id: (key: KeyListEntry) => key.KeyArn ?? key.KeyId ?? "", + describe: (client, identifier) => + client.send(new DescribeKeyCommand({ KeyId: identifier.arn })), + detail: [ + { label: "Key ID", path: "KeyMetadata.KeyId", type: FieldType.NAME }, + { label: "ARN", path: "KeyMetadata.Arn", type: FieldType.ARN }, + { + label: "Description", + path: "KeyMetadata.Description", + type: FieldType.SHORT_TEXT, + }, + { label: "State", path: "KeyMetadata.KeyState", type: FieldType.NAME }, + { label: "Usage", path: "KeyMetadata.KeyUsage", type: FieldType.NAME }, + { label: "Enabled", path: "KeyMetadata.Enabled", type: FieldType.NAME }, + { + label: "Creation Date", + path: "KeyMetadata.CreationDate", + type: FieldType.DATE, + }, + ], + }, + alias: { + singular: "Alias", + plural: "Aliases", + metamodelOp: "listAliases", + cfn: "AWS::KMS::Alias", + list: async (client): Promise => { + const aliases: AliasListEntry[] = []; + let marker: string | undefined; + do { + const out = await client.send( + new ListAliasesCommand({ Marker: marker }), + ); + aliases.push(...(out.Aliases ?? [])); + marker = out.Truncated ? out.NextMarker : undefined; + } while (marker); + return aliases; + }, + id: (alias: AliasListEntry) => alias.AliasArn ?? alias.AliasName ?? "", + /* No GetAlias API; detail comes from the matching ListAliases item. */ + detail: [ + { label: "Alias Name", path: "AliasName", type: FieldType.NAME }, + { label: "ARN", path: "AliasArn", type: FieldType.ARN }, + { label: "Target Key ID", path: "TargetKeyId", type: FieldType.NAME }, + { + label: "Creation Date", + path: "CreationDate", + type: FieldType.DATE, + }, + { + label: "Last Updated Date", + path: "LastUpdatedDate", + type: FieldType.DATE, + }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/lambda.ts b/src/platforms/aws/services/definitions/lambda.ts new file mode 100644 index 0000000..c6f22fb --- /dev/null +++ b/src/platforms/aws/services/definitions/lambda.ts @@ -0,0 +1,171 @@ +import { + GetEventSourceMappingCommand, + GetFunctionCommand, + LambdaClient, + ListEventSourceMappingsCommand, + ListFunctionsCommand, +} from "@aws-sdk/client-lambda"; +import type { + EventSourceMappingConfiguration, + FunctionConfiguration, +} from "@aws-sdk/client-lambda"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * Lambda. Functions and event source mappings. Both list operations paginate on + * `Marker`/`NextMarker`. The CloudFormation resource name encodes the type + * (`function:` / `event-source-mapping:`) so the synthesized ARN resolves back + * to the right resource type. Array fields (architectures, response types) are + * joined into a single display string by `describe`. + */ +export const lambdaDefinition = defineService({ + id: "lambda", + name: "Lambda", + client: (config) => new LambdaClient(config), + resourceTypes: { + function: { + singular: "Function", + plural: "Functions", + metamodelOp: "listFunctions", + cfn: "AWS::Lambda::Function", + cfnResourceName: (summary) => + summary.PhysicalResourceId + ? `function:${summary.PhysicalResourceId}` + : undefined, + list: async (client): Promise => { + const functions: FunctionConfiguration[] = []; + let marker: string | undefined; + do { + const out = await client.send( + new ListFunctionsCommand({ Marker: marker }), + ); + functions.push(...(out.Functions ?? [])); + marker = out.NextMarker; + } while (marker); + return functions; + }, + id: (fn: FunctionConfiguration) => fn.FunctionArn ?? "", + describe: async (client, identifier) => { + const out = await client.send( + new GetFunctionCommand({ FunctionName: identifier.resourceName }), + ); + const config = out.Configuration ?? {}; + return { + ...config, + ArchitecturesText: (config.Architectures ?? []).join(", "), + }; + }, + detail: [ + { label: "Name", path: "FunctionName", type: FieldType.NAME }, + { label: "State", path: "State", type: FieldType.NAME }, + { label: "Description", path: "Description", type: FieldType.NAME }, + { label: "Runtime", path: "Runtime", type: FieldType.NAME }, + { label: "Handler", path: "Handler", type: FieldType.NAME }, + { label: "Version", path: "Version", type: FieldType.NAME }, + { label: "Role", path: "Role", type: FieldType.ARN }, + { + label: "Code Size (bytes)", + path: "CodeSize", + type: FieldType.NUMBER, + }, + { + label: "Memory Size (MB)", + path: "MemorySize", + type: FieldType.NUMBER, + }, + { label: "Timeout (seconds)", path: "Timeout", type: FieldType.NUMBER }, + { label: "Last Modified", path: "LastModified", type: FieldType.DATE }, + { + label: "Last Update Status", + path: "LastUpdateStatus", + type: FieldType.NAME, + }, + { label: "Package Type", path: "PackageType", type: FieldType.NAME }, + { + label: "Architectures", + path: "ArchitecturesText", + type: FieldType.NAME, + }, + { + label: "LogFormat", + path: "LoggingConfig.LogFormat", + type: FieldType.NAME, + }, + { + label: "LogGroup", + path: "LoggingConfig.LogGroup", + type: FieldType.LOG_GROUP, + }, + ], + }, + "event-source-mapping": { + singular: "Event Source Mapping", + plural: "Event Source Mappings", + metamodelOp: "listEventSourceMappings", + cfn: "AWS::Lambda::EventSourceMapping", + cfnResourceName: (summary) => + summary.PhysicalResourceId + ? `event-source-mapping:${summary.PhysicalResourceId}` + : undefined, + list: async (client): Promise => { + const mappings: EventSourceMappingConfiguration[] = []; + let marker: string | undefined; + do { + const out = await client.send( + new ListEventSourceMappingsCommand({ Marker: marker }), + ); + mappings.push(...(out.EventSourceMappings ?? [])); + marker = out.NextMarker; + } while (marker); + return mappings; + }, + id: (mapping: EventSourceMappingConfiguration) => + mapping.EventSourceMappingArn ?? "", + describe: async (client, identifier) => { + const mapping = await client.send( + new GetEventSourceMappingCommand({ UUID: identifier.resourceName }), + ); + return { + ...mapping, + FunctionResponseTypesText: (mapping.FunctionResponseTypes ?? []).join( + ", ", + ), + }; + }, + detail: [ + { label: "UUID", path: "UUID", type: FieldType.NAME }, + { label: "Function ARN", path: "FunctionArn", type: FieldType.ARN }, + { + label: "Event Source ARN", + path: "EventSourceArn", + type: FieldType.ARN, + }, + { label: "State", path: "State", type: FieldType.NAME }, + { + label: "State Transition Reason", + path: "StateTransitionReason", + type: FieldType.NAME, + }, + { label: "Batch Size", path: "BatchSize", type: FieldType.NUMBER }, + { + label: "Maximum Batching Window (seconds)", + path: "MaximumBatchingWindowInSeconds", + type: FieldType.NUMBER, + }, + { label: "Last Modified", path: "LastModified", type: FieldType.DATE }, + { + label: "Last Processing Result", + path: "LastProcessingResult", + type: FieldType.NAME, + }, + { + label: "Function Response Types", + path: "FunctionResponseTypesText", + type: FieldType.NAME, + }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/logs.ts b/src/platforms/aws/services/definitions/logs.ts new file mode 100644 index 0000000..69904d1 --- /dev/null +++ b/src/platforms/aws/services/definitions/logs.ts @@ -0,0 +1,215 @@ +import { + CloudWatchLogsClient, + DescribeDestinationsCommand, + DescribeLogGroupsCommand, + DescribeLogStreamsCommand, + DescribeMetricFiltersCommand, + DescribeSubscriptionFiltersCommand, +} from "@aws-sdk/client-cloudwatch-logs"; +import type { + Destination, + LogGroup, + LogStream, + MetricFilter, + SubscriptionFilter, +} from "@aws-sdk/client-cloudwatch-logs"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * CloudWatch Logs. Log groups, log streams (enumerated across groups), metric + * filters, subscription filters (enumerated across groups), and destinations. + * Log groups/streams/destinations carry ARNs; the two filter types do not, so + * an account-less ARN is synthesized and detail is read from the list item + * (matched back by identifier — the synthesized ARN is never re-parsed). + */ +export const logsDefinition = defineService({ + id: "logs", + name: "CloudWatch Logs", + client: (config) => new CloudWatchLogsClient(config), + resourceTypes: { + loggroup: { + singular: "Log Group", + plural: "Log Groups", + metamodelOp: "describeLogGroups", + cfn: "AWS::Logs::LogGroup", + matchArn: (identifier) => + identifier.arn.includes(":log-group:") && + !identifier.arn.includes(":log-stream:"), + list: async (client): Promise => { + const groups: LogGroup[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new DescribeLogGroupsCommand({ nextToken }), + ); + groups.push(...(out.logGroups ?? [])); + nextToken = out.nextToken; + } while (nextToken); + return groups; + }, + id: (group: LogGroup) => group.arn ?? group.logGroupName ?? "", + detail: [ + { label: "Name", path: "logGroupName", type: FieldType.LOG_GROUP }, + { label: "ARN", path: "arn", type: FieldType.ARN }, + { + label: "Retention (days)", + path: "retentionInDays", + type: FieldType.NUMBER, + }, + { label: "Stored Bytes", path: "storedBytes", type: FieldType.NUMBER }, + { + label: "Metric Filters", + path: "metricFilterCount", + type: FieldType.NUMBER, + }, + { label: "Creation Time", path: "creationTime", type: FieldType.DATE }, + ], + }, + logstream: { + singular: "Log Stream", + plural: "Log Streams", + metamodelOp: "describeLogStreams", + matchArn: (identifier) => identifier.arn.includes(":log-stream:"), + list: async (client): Promise => { + const groupNames = await listLogGroupNames(client); + const streams: LogStream[] = []; + for (const logGroupName of groupNames) { + let nextToken: string | undefined; + do { + const out = await client.send( + new DescribeLogStreamsCommand({ logGroupName, nextToken }), + ); + streams.push(...(out.logStreams ?? [])); + nextToken = out.nextToken; + } while (nextToken); + } + return streams; + }, + id: (stream: LogStream) => stream.arn ?? stream.logStreamName ?? "", + detail: [ + { label: "Name", path: "logStreamName", type: FieldType.NAME }, + { label: "ARN", path: "arn", type: FieldType.ARN }, + { label: "Creation Time", path: "creationTime", type: FieldType.DATE }, + { + label: "Last Event", + path: "lastEventTimestamp", + type: FieldType.DATE, + }, + ], + }, + metricfilter: { + singular: "Metric Filter", + plural: "Metric Filters", + metamodelOp: "describeMetricFilters", + cfn: "AWS::Logs::MetricFilter", + matchArn: (identifier) => identifier.arn.includes(":metric-filter:"), + list: async (client): Promise => { + const filters: MetricFilter[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new DescribeMetricFiltersCommand({ nextToken }), + ); + filters.push(...(out.metricFilters ?? [])); + nextToken = out.nextToken; + } while (nextToken); + return filters; + }, + id: (filter: MetricFilter, ctx) => + `arn:aws:logs:${ctx.region}::metric-filter:${filter.logGroupName}:${filter.filterName}`, + detail: [ + { label: "Name", path: "filterName", type: FieldType.NAME }, + { label: "Log Group", path: "logGroupName", type: FieldType.LOG_GROUP }, + { label: "Pattern", path: "filterPattern", type: FieldType.SHORT_TEXT }, + { label: "Creation Time", path: "creationTime", type: FieldType.DATE }, + ], + }, + subscriptionfilter: { + singular: "Subscription Filter", + plural: "Subscription Filters", + metamodelOp: "describeSubscriptionFilters", + cfn: "AWS::Logs::SubscriptionFilter", + matchArn: (identifier) => + identifier.arn.includes(":subscription-filter:"), + list: async (client): Promise => { + const groupNames = await listLogGroupNames(client); + const filters: SubscriptionFilter[] = []; + for (const logGroupName of groupNames) { + let nextToken: string | undefined; + do { + const out = await client.send( + new DescribeSubscriptionFiltersCommand({ + logGroupName, + nextToken, + }), + ); + filters.push(...(out.subscriptionFilters ?? [])); + nextToken = out.nextToken; + } while (nextToken); + } + return filters; + }, + id: (filter: SubscriptionFilter, ctx) => + `arn:aws:logs:${ctx.region}::subscription-filter:${filter.logGroupName}:${filter.filterName}`, + detail: [ + { label: "Name", path: "filterName", type: FieldType.NAME }, + { label: "Log Group", path: "logGroupName", type: FieldType.LOG_GROUP }, + { label: "Pattern", path: "filterPattern", type: FieldType.SHORT_TEXT }, + { + label: "Destination ARN", + path: "destinationArn", + type: FieldType.ARN, + }, + { label: "Creation Time", path: "creationTime", type: FieldType.DATE }, + ], + }, + destination: { + singular: "Destination", + plural: "Destinations", + metamodelOp: "describeDestinations", + cfn: "AWS::Logs::Destination", + matchArn: (identifier) => identifier.arn.includes(":destination:"), + list: async (client): Promise => { + const destinations: Destination[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new DescribeDestinationsCommand({ nextToken }), + ); + destinations.push(...(out.destinations ?? [])); + nextToken = out.nextToken; + } while (nextToken); + return destinations; + }, + id: (destination: Destination) => + destination.arn ?? destination.destinationName ?? "", + detail: [ + { label: "Name", path: "destinationName", type: FieldType.NAME }, + { label: "ARN", path: "arn", type: FieldType.ARN }, + { label: "Target ARN", path: "targetArn", type: FieldType.ARN }, + { label: "Role ARN", path: "roleArn", type: FieldType.ARN }, + { label: "Creation Time", path: "creationTime", type: FieldType.DATE }, + ], + }, + }, +}); + +/** Enumerate every log group name (for resource types scoped to a group). */ +async function listLogGroupNames( + client: CloudWatchLogsClient, +): Promise { + const names: string[] = []; + let nextToken: string | undefined; + do { + const out = await client.send(new DescribeLogGroupsCommand({ nextToken })); + names.push( + ...(out.logGroups ?? []) + .map((group) => group.logGroupName) + .filter((name): name is string => Boolean(name)), + ); + nextToken = out.nextToken; + } while (nextToken); + return names; +} diff --git a/src/platforms/aws/services/definitions/s3.ts b/src/platforms/aws/services/definitions/s3.ts new file mode 100644 index 0000000..220f82c --- /dev/null +++ b/src/platforms/aws/services/definitions/s3.ts @@ -0,0 +1,40 @@ +import { ListBucketsCommand, S3Client } from "@aws-sdk/client-s3"; +import type { Bucket } from "@aws-sdk/client-s3"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * S3. Buckets are the only browsable resource type. S3 has no single-call + * "describe bucket" that returns everything, so detail is drawn from the + * ListBuckets item (name + creation date) as a first cut; richer per-bucket + * detail (versioning, encryption, …) would need extra calls and can be added + * later. + */ +export const s3Definition = defineService({ + id: "s3", + name: "S3", + client: (config) => new S3Client(config), + resourceTypes: { + bucket: { + singular: "Bucket", + plural: "Buckets", + cfn: "AWS::S3::Bucket", + list: async (client): Promise => { + const out = await client.send(new ListBucketsCommand({})); + return out.Buckets ?? []; + }, + id: (bucket: Bucket) => `arn:aws:s3:::${bucket.Name}`, + describe: async (client, identifier) => { + const out = await client.send(new ListBucketsCommand({})); + return (out.Buckets ?? []).find( + (bucket) => bucket.Name === identifier.resourceName, + ); + }, + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { label: "Creation Date", path: "CreationDate", type: FieldType.DATE }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/secretsmanager.ts b/src/platforms/aws/services/definitions/secretsmanager.ts new file mode 100644 index 0000000..b2e3878 --- /dev/null +++ b/src/platforms/aws/services/definitions/secretsmanager.ts @@ -0,0 +1,58 @@ +import { + DescribeSecretCommand, + ListSecretsCommand, + SecretsManagerClient, +} from "@aws-sdk/client-secrets-manager"; +import type { SecretListEntry } from "@aws-sdk/client-secrets-manager"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** Secrets Manager. Secrets are identified by their full ARN. */ +export const secretsManagerDefinition = defineService({ + id: "secretsmanager", + name: "Secrets Manager", + client: (config) => new SecretsManagerClient(config), + resourceTypes: { + secret: { + singular: "Secret", + plural: "Secrets", + cfn: "AWS::SecretsManager::Secret", + list: async (client): Promise => { + const secrets: SecretListEntry[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListSecretsCommand({ NextToken: nextToken }), + ); + secrets.push(...(out.SecretList ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return secrets; + }, + id: (secret: SecretListEntry) => secret.ARN ?? secret.Name ?? "", + describe: (client, identifier) => + client.send(new DescribeSecretCommand({ SecretId: identifier.arn })), + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { label: "ARN", path: "ARN", type: FieldType.ARN }, + { + label: "Description", + path: "Description", + type: FieldType.SHORT_TEXT, + }, + { + label: "Rotation Enabled", + path: "RotationEnabled", + type: FieldType.NAME, + }, + { + label: "Last Changed Date", + path: "LastChangedDate", + type: FieldType.DATE, + }, + { label: "Created Date", path: "CreatedDate", type: FieldType.DATE }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/sns.ts b/src/platforms/aws/services/definitions/sns.ts new file mode 100644 index 0000000..fb24a3b --- /dev/null +++ b/src/platforms/aws/services/definitions/sns.ts @@ -0,0 +1,70 @@ +import { + GetTopicAttributesCommand, + ListTopicsCommand, + SNSClient, +} from "@aws-sdk/client-sns"; +import type { Topic } from "@aws-sdk/client-sns"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * SNS. Topics only; the topic name is not a distinct ARN segment, so the detail + * name is taken from the ARN's resource name and the rest from the topic's + * attributes (`GetTopicAttributes`). + */ +export const snsDefinition = defineService({ + id: "sns", + name: "SNS", + client: (config) => new SNSClient(config), + resourceTypes: { + topic: { + singular: "Topic", + plural: "Topics", + list: async (client): Promise => { + const topics: Topic[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListTopicsCommand({ NextToken: nextToken }), + ); + topics.push(...(out.Topics ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return topics; + }, + id: (topic: Topic) => topic.TopicArn ?? "", + describe: async (client, identifier) => { + const out = await client.send( + new GetTopicAttributesCommand({ TopicArn: identifier.arn }), + ); + return { Name: identifier.resourceName, ...(out.Attributes ?? {}) }; + }, + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { label: "Display Name", path: "DisplayName", type: FieldType.NAME }, + { + label: "Subscriptions Confirmed", + path: "SubscriptionsConfirmed", + type: FieldType.NUMBER, + }, + { + label: "Subscriptions Pending", + path: "SubscriptionsPending", + type: FieldType.NUMBER, + }, + { + label: "Subscriptions Deleted", + path: "SubscriptionsDeleted", + type: FieldType.NUMBER, + }, + { label: "Policy", path: "Policy", type: FieldType.JSON }, + { + label: "Effective Delivery Policy", + path: "EffectiveDeliveryPolicy", + type: FieldType.JSON, + }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/sqs.ts b/src/platforms/aws/services/definitions/sqs.ts new file mode 100644 index 0000000..56dfaaf --- /dev/null +++ b/src/platforms/aws/services/definitions/sqs.ts @@ -0,0 +1,149 @@ +import { + GetQueueAttributesCommand, + GetQueueUrlCommand, + ListQueuesCommand, + SQSClient, +} from "@aws-sdk/client-sqs"; + +import { InternalError } from "../../../../utils/errors.ts"; +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * SQS. Queues only. SQS does not use standard ARNs for querying, so `list` + * resolves each queue URL to its ARN via `GetQueueAttributes`, and `describe` + * converts the ARN back to a URL via `GetQueueUrl` before reading attributes. + * The two timestamps come back as epoch-second strings; they are coerced to + * numbers so the `DATE` formatter renders them as ISO dates. + */ +export const sqsDefinition = defineService({ + id: "sqs", + name: "SQS", + client: (config) => new SQSClient(config), + resourceTypes: { + queue: { + singular: "Queue", + plural: "Queues", + cfn: "AWS::SQS::Queue", + cfnResourceName: (summary) => + summary.PhysicalResourceId?.split("/").pop(), + list: async (client): Promise => { + const urls: string[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListQueuesCommand({ NextToken: nextToken }), + ); + urls.push(...(out.QueueUrls ?? [])); + nextToken = out.NextToken; + } while (nextToken); + /* The list API returns URLs; resolve each to its ARN (correct across + * non-standard partitions). */ + return Promise.all( + urls.map(async (url) => { + const attrs = await client.send( + new GetQueueAttributesCommand({ + QueueUrl: url, + AttributeNames: ["QueueArn"], + }), + ); + const queueArn = attrs.Attributes?.QueueArn; + if (!queueArn) { + throw new InternalError( + `Failed to resolve ARN for SQS queue: ${url}`, + ); + } + return queueArn; + }), + ); + }, + id: (arn: string) => arn, + describe: async (client, identifier) => { + const urlOut = await client.send( + new GetQueueUrlCommand({ QueueName: identifier.resourceName }), + ); + const queueUrl = urlOut.QueueUrl; + if (!queueUrl) { + throw new InternalError( + `Failed to resolve URL for SQS queue: ${identifier.resourceName}`, + ); + } + const out = await client.send( + new GetQueueAttributesCommand({ + QueueUrl: queueUrl, + AttributeNames: ["All"], + }), + ); + const attrs = out.Attributes ?? {}; + return { + Name: identifier.resourceName, + ...attrs, + CreatedTimestamp: attrs.CreatedTimestamp + ? Number(attrs.CreatedTimestamp) + : undefined, + LastModifiedTimestamp: attrs.LastModifiedTimestamp + ? Number(attrs.LastModifiedTimestamp) + : undefined, + }; + }, + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { + label: "Visibility Timeout", + path: "VisibilityTimeout", + type: FieldType.NUMBER, + }, + { + label: "Maximum Message Size", + path: "MaximumMessageSize", + type: FieldType.NUMBER, + }, + { + label: "Message Retention Period", + path: "MessageRetentionPeriod", + type: FieldType.NUMBER, + }, + { + label: "Delay Seconds", + path: "DelaySeconds", + type: FieldType.NUMBER, + }, + { + label: "Receive Message Wait Time Seconds", + path: "ReceiveMessageWaitTimeSeconds", + type: FieldType.NUMBER, + }, + { + label: "SQS Managed SSE Enabled", + path: "SqsManagedSseEnabled", + type: FieldType.NAME, + }, + { + label: "Approximate Number of Messages", + path: "ApproximateNumberOfMessages", + type: FieldType.NUMBER, + }, + { + label: "Approximate Number of Messages Delayed", + path: "ApproximateNumberOfMessagesDelayed", + type: FieldType.NUMBER, + }, + { + label: "Approximate Number of Messages Not Visible", + path: "ApproximateNumberOfMessagesNotVisible", + type: FieldType.NUMBER, + }, + { + label: "Created Timestamp", + path: "CreatedTimestamp", + type: FieldType.DATE, + }, + { + label: "Last Modified Timestamp", + path: "LastModifiedTimestamp", + type: FieldType.DATE, + }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/ssm.ts b/src/platforms/aws/services/definitions/ssm.ts new file mode 100644 index 0000000..a2f761c --- /dev/null +++ b/src/platforms/aws/services/definitions/ssm.ts @@ -0,0 +1,234 @@ +import { + DescribeDocumentCommand, + DescribeMaintenanceWindowsCommand, + DescribeParametersCommand, + DescribePatchBaselinesCommand, + ListAssociationsCommand, + ListDocumentsCommand, + SSMClient, +} from "@aws-sdk/client-ssm"; +import type { + Association, + DocumentIdentifier, + ParameterMetadata, + PatchBaselineIdentity, + MaintenanceWindowIdentity, +} from "@aws-sdk/client-ssm"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * SSM (Systems Manager). Parameters, documents, maintenance windows, + * associations, and patch baselines. Most of these list responses omit an ARN, + * so an account-less ARN is synthesized from the region and the resource's + * primary id; the resource segment distinguishes the five types. + */ +export const ssmDefinition = defineService({ + id: "ssm", + name: "SSM", + client: (config) => new SSMClient(config), + resourceTypes: { + parameter: { + singular: "Parameter", + plural: "Parameters", + metamodelOp: "describeParameters", + cfn: "AWS::SSM::Parameter", + matchArn: (identifier) => identifier.arn.includes(":parameter/"), + list: async (client): Promise => { + const parameters: ParameterMetadata[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new DescribeParametersCommand({ NextToken: nextToken }), + ); + parameters.push(...(out.Parameters ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return parameters; + }, + id: (parameter: ParameterMetadata, ctx) => + `arn:aws:ssm:${ctx.region}::parameter/${parameter.Name}`, + describe: async (client, identifier) => { + const out = await client.send( + new DescribeParametersCommand({ + ParameterFilters: [ + { Key: "Name", Values: [identifier.resourceName ?? ""] }, + ], + }), + ); + return out.Parameters?.[0]; + }, + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { label: "Type", path: "Type", type: FieldType.NAME }, + { label: "Tier", path: "Tier", type: FieldType.NAME }, + { label: "Version", path: "Version", type: FieldType.NUMBER }, + { + label: "Description", + path: "Description", + type: FieldType.SHORT_TEXT, + }, + { + label: "Last Modified", + path: "LastModifiedDate", + type: FieldType.DATE, + }, + ], + }, + document: { + singular: "Document", + plural: "Documents", + metamodelOp: "listDocuments", + cfn: "AWS::SSM::Document", + matchArn: (identifier) => identifier.arn.includes(":document/"), + list: async (client): Promise => { + const documents: DocumentIdentifier[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListDocumentsCommand({ NextToken: nextToken }), + ); + documents.push(...(out.DocumentIdentifiers ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return documents; + }, + id: (document: DocumentIdentifier, ctx) => + `arn:aws:ssm:${ctx.region}::document/${document.Name}`, + describe: (client, identifier) => + client.send( + new DescribeDocumentCommand({ Name: identifier.resourceName }), + ), + detail: [ + { label: "Name", path: "Document.Name", type: FieldType.NAME }, + { label: "Type", path: "Document.DocumentType", type: FieldType.NAME }, + { + label: "Format", + path: "Document.DocumentFormat", + type: FieldType.NAME, + }, + { label: "Status", path: "Document.Status", type: FieldType.NAME }, + { label: "Owner", path: "Document.Owner", type: FieldType.NAME }, + { + label: "Created Date", + path: "Document.CreatedDate", + type: FieldType.DATE, + }, + ], + }, + maintenancewindow: { + singular: "Maintenance Window", + plural: "Maintenance Windows", + metamodelOp: "describeMaintenanceWindows", + cfn: "AWS::SSM::MaintenanceWindow", + matchArn: (identifier) => identifier.arn.includes(":maintenancewindow/"), + list: async (client): Promise => { + const windows: MaintenanceWindowIdentity[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new DescribeMaintenanceWindowsCommand({ NextToken: nextToken }), + ); + windows.push(...(out.WindowIdentities ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return windows; + }, + id: (window: MaintenanceWindowIdentity, ctx) => + `arn:aws:ssm:${ctx.region}::maintenancewindow/${window.WindowId}`, + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { label: "Window ID", path: "WindowId", type: FieldType.NAME }, + { label: "Enabled", path: "Enabled", type: FieldType.NAME }, + { label: "Duration", path: "Duration", type: FieldType.NUMBER }, + { label: "Cutoff", path: "Cutoff", type: FieldType.NUMBER }, + { + label: "Schedule", + path: "ScheduleExpression", + type: FieldType.SHORT_TEXT, + }, + ], + }, + association: { + singular: "Association", + plural: "Associations", + metamodelOp: "listAssociations", + cfn: "AWS::SSM::Association", + matchArn: (identifier) => identifier.arn.includes(":association/"), + list: async (client): Promise => { + const associations: Association[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListAssociationsCommand({ NextToken: nextToken }), + ); + associations.push(...(out.Associations ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return associations; + }, + id: (association: Association, ctx) => + `arn:aws:ssm:${ctx.region}::association/${association.AssociationId}`, + detail: [ + { + label: "Association ID", + path: "AssociationId", + type: FieldType.NAME, + }, + { label: "Name", path: "Name", type: FieldType.NAME }, + { + label: "Association Name", + path: "AssociationName", + type: FieldType.NAME, + }, + { + label: "Schedule", + path: "ScheduleExpression", + type: FieldType.SHORT_TEXT, + }, + { + label: "Last Execution", + path: "LastExecutionDate", + type: FieldType.DATE, + }, + ], + }, + patchbaseline: { + singular: "Patch Baseline", + plural: "Patch Baselines", + metamodelOp: "describePatchBaselines", + cfn: "AWS::SSM::PatchBaseline", + matchArn: (identifier) => identifier.arn.includes(":patchbaseline/"), + list: async (client): Promise => { + const baselines: PatchBaselineIdentity[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new DescribePatchBaselinesCommand({ NextToken: nextToken }), + ); + baselines.push(...(out.BaselineIdentities ?? [])); + nextToken = out.NextToken; + } while (nextToken); + return baselines; + }, + id: (baseline: PatchBaselineIdentity, ctx) => + `arn:aws:ssm:${ctx.region}::patchbaseline/${baseline.BaselineId}`, + detail: [ + { label: "Name", path: "BaselineName", type: FieldType.NAME }, + { label: "Baseline ID", path: "BaselineId", type: FieldType.NAME }, + { + label: "Operating System", + path: "OperatingSystem", + type: FieldType.NAME, + }, + { + label: "Description", + path: "BaselineDescription", + type: FieldType.SHORT_TEXT, + }, + { label: "Default", path: "DefaultBaseline", type: FieldType.NAME }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/definitions/states.ts b/src/platforms/aws/services/definitions/states.ts new file mode 100644 index 0000000..0a7194a --- /dev/null +++ b/src/platforms/aws/services/definitions/states.ts @@ -0,0 +1,113 @@ +import { + DescribeStateMachineCommand, + ListActivitiesCommand, + ListStateMachinesCommand, + SFNClient, +} from "@aws-sdk/client-sfn"; +import type { + ActivityListItem, + StateMachineListItem, +} from "@aws-sdk/client-sfn"; + +import { defineService } from "../declarative/types.ts"; +import { FieldType } from "../serviceProvider.ts"; + +/** + * Step Functions (service code `states`). Activities self-describe from their + * list item; state machines are described via `DescribeStateMachine`, whose + * optional logging/tracing config is flattened into display strings. The state + * machine ARN's resource token is `stateMachine`, matched case-insensitively to + * the `statemachine` resource-type id. No CloudFormation mapping (parity with + * the previous provider, which did not support it). + */ +export const statesDefinition = defineService({ + id: "states", + name: "Step Functions", + client: (config) => new SFNClient(config), + resourceTypes: { + activity: { + singular: "Activity", + plural: "Activities", + metamodelOp: "listActivities", + list: async (client): Promise => { + const activities: ActivityListItem[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListActivitiesCommand({ nextToken }), + ); + activities.push(...(out.activities ?? [])); + nextToken = out.nextToken; + } while (nextToken); + return activities; + }, + id: (activity: ActivityListItem) => activity.activityArn ?? "", + /* Self-describe: the list item carries name + creationDate. */ + detail: [ + { label: "Name", path: "name", type: FieldType.NAME }, + { label: "Creation Date", path: "creationDate", type: FieldType.DATE }, + ], + }, + statemachine: { + singular: "State Machine", + plural: "State Machines", + metamodelOp: "listStateMachines", + list: async (client): Promise => { + const machines: StateMachineListItem[] = []; + let nextToken: string | undefined; + do { + const out = await client.send( + new ListStateMachinesCommand({ nextToken }), + ); + machines.push(...(out.stateMachines ?? [])); + nextToken = out.nextToken; + } while (nextToken); + return machines; + }, + id: (machine: StateMachineListItem) => machine.stateMachineArn ?? "", + describe: async (client, identifier) => { + const details = await client.send( + new DescribeStateMachineCommand({ stateMachineArn: identifier.arn }), + ); + const destination = details.loggingConfiguration?.destinations?.[0]; + return { + ...details, + LogGroupArn: + destination?.cloudWatchLogsLogGroup?.logGroupArn ?? "None", + LogExecutionDataText: details.loggingConfiguration + ?.includeExecutionData + ? "Yes" + : "No", + TracingText: details.tracingConfiguration?.enabled + ? "Enabled" + : "Disabled", + }; + }, + detail: [ + { label: "Name", path: "name", type: FieldType.NAME }, + { + label: "Description", + path: "description", + type: FieldType.SHORT_TEXT, + }, + { label: "State Machine Type", path: "type", type: FieldType.NAME }, + { label: "Status", path: "status", type: FieldType.NAME }, + { label: "Creation Date", path: "creationDate", type: FieldType.DATE }, + { label: "Role ARN", path: "roleArn", type: FieldType.ARN }, + { label: "Definition", path: "definition", type: FieldType.JSON }, + { label: "Log Group", path: "LogGroupArn", type: FieldType.ARN }, + { + label: "Log Execution Data", + path: "LogExecutionDataText", + type: FieldType.NAME, + }, + { + label: "Log Level", + path: "loggingConfiguration.level", + type: FieldType.NAME, + }, + { label: "Tracing", path: "TracingText", type: FieldType.NAME }, + ], + }, + }, +}); diff --git a/src/platforms/aws/services/providerFactory.ts b/src/platforms/aws/services/providerFactory.ts new file mode 100644 index 0000000..a123c45 --- /dev/null +++ b/src/platforms/aws/services/providerFactory.ts @@ -0,0 +1,105 @@ +import { InternalError } from "../../../utils/errors.ts"; + +import { DeclarativeServiceProvider } from "./declarative/engine.ts"; +import { serviceDefinitions } from "./definitions/index.ts"; +import type { ServiceProvider } from "./serviceProvider.ts"; + +/** + * Imperative `ServiceProvider` subclasses: the escape hatch for services that + * cannot be expressed declaratively. Every service is currently declarative, so + * this is empty — but the registration path is kept so a future service that + * needs hand-written logic can be added here and register identically to a + * declarative provider (both end up as `ServiceProvider` instances in the same + * map). New `() => ServiceProvider` constructors go in this array. + */ +const IMPERATIVE_PROVIDERS: (new () => ServiceProvider)[] = []; + +/** + * Mapping of AWS service IDs to their providers. + */ +let providers: Map; + +/** + * Sorted array of providers (same as providers map, but sorted by name). + */ +let providersArray: ServiceProvider[]; + +/** + * A factory for providing access to AWS service providers. + * + * Providers are resolved by manifest service id. A manifest service with no + * registered provider is simply absent — there is no generic/fallback provider. + */ +export const ProviderFactory = { + /** + * Initialize the ProviderFactory so it's able to provide service provider + * instances. Both declarative definitions and imperative subclasses are + * registered into the same map, keyed by their service id. + */ + initialize() { + const map = new Map(); + + /* Declarative providers (data-authored, executed by the engine). */ + for (const definition of serviceDefinitions) { + map.set(definition.id, new DeclarativeServiceProvider(definition)); + } + + /* Imperative providers (escape hatch). */ + for (const Provider of IMPERATIVE_PROVIDERS) { + const provider = new Provider(); + map.set(provider.getId(), provider); + } + + providers = map; + providersArray = Array.from(map.values()).sort((a, b) => + a.getName().localeCompare(b.getName()), + ); + }, + + /** + * Get the service provider for a given service ID. It should not be + * possible to pass an illegal name into this method, so treat it like + * an internal error. + * + * @param id The service ID. + * @returns The service provider for the given service ID. + */ + getProviderForService(id: string): ServiceProvider { + const provider = providers.get(id); + if (!provider) { + throw new InternalError(`Unhandled service: ${id}`); + } + return provider; + }, + + /** + * Return the provider for a service id, or `undefined` if no provider is + * registered (e.g. a manifest service not yet curated). Callers that should + * tolerate uncurated services use this and skip/log the absence. + */ + tryGetProviderForService(id: string): ServiceProvider | undefined { + return providers.get(id); + }, + + /** + * Whether a provider is registered for the given service id. + */ + hasProviderForService(id: string): boolean { + return providers.has(id); + }, + + /** + * Return every registered provider's service id. + */ + getRegisteredServiceIds(): string[] { + return [...providers.keys()]; + }, + + /** + * Return the complete list of supported ServiceProviders, in alphabetical + * (display) order. + */ + getSupportedServices(): ServiceProvider[] { + return providersArray; + }, +}; diff --git a/src/platforms/aws/services/serviceManifest.ts b/src/platforms/aws/services/serviceManifest.ts new file mode 100644 index 0000000..edcf8f0 --- /dev/null +++ b/src/platforms/aws/services/serviceManifest.ts @@ -0,0 +1,94 @@ +/* + * The static service manifest: the single source of truth for which AWS + * services the resource browser knows about. It is generated on demand from + * LocalStack's published coverage data by `build/generate-service-manifest.mjs` + * and committed to `resources/service-manifest.json`; nothing here queries a + * running emulator or any discovery API. Availability (community/pro) is + * deliberately neither stored nor exposed — because the browser also targets + * real AWS, every service is treated as fully available. + */ +import manifestData from "../../../../resources/service-manifest.json"; +import { InternalError } from "../../../utils/errors.ts"; + +/** A single manifest entry: AWS service-code id + display name. */ +export type ServiceManifestEntry = { + /** AWS service code (SDK/endpoint id), e.g. `s3`, `logs`, `states`. */ + id: string; + /** Human-readable display name, e.g. `S3`, `CloudWatch Logs`. */ + name: string; +}; + +/** + * Service labels as they appear in emulator data (metamodel PascalCase labels, + * CloudFormation resource-type namespaces) do not always equal the manifest id + * under a simple lowercase transform. This documented override table maps the + * exceptions. Keyed by the lowercased label. + */ +const LABEL_OVERRIDES: Record = { + /* Coverage/CFN/metamodel call it "StepFunctions"; the AWS service code is `states`. */ + stepfunctions: "states", +}; + +let memoizedEntries: ServiceManifestEntry[] | undefined; +let memoizedById: Map | undefined; + +function load(): { + entries: ServiceManifestEntry[]; + byId: Map; +} { + if (memoizedEntries && memoizedById) { + return { entries: memoizedEntries, byId: memoizedById }; + } + + const services = (manifestData as { services?: unknown }).services; + if (!Array.isArray(services)) { + throw new InternalError( + "service-manifest.json is malformed: missing `services` array", + ); + } + + const byId = new Map(); + for (const entry of services) { + if ( + typeof entry !== "object" || + entry === null || + typeof (entry as ServiceManifestEntry).id !== "string" || + typeof (entry as ServiceManifestEntry).name !== "string" + ) { + throw new InternalError( + `service-manifest.json has a malformed entry: ${JSON.stringify(entry)}`, + ); + } + const valid = entry as ServiceManifestEntry; + byId.set(valid.id, valid); + } + + memoizedEntries = [...byId.values()]; + memoizedById = byId; + return { entries: memoizedEntries, byId: memoizedById }; +} + +/** Return every manifest entry. */ +export function getManifest(): ServiceManifestEntry[] { + return load().entries; +} + +/** Look up a single manifest entry by service id, or `undefined` if absent. */ +export function getEntry(id: string): ServiceManifestEntry | undefined { + return load().byId.get(id); +} + +/** Return every manifest service id. */ +export function getAllServiceIds(): string[] { + return load().entries.map((entry) => entry.id); +} + +/** + * Map a service label found in emulator data (metamodel label, CloudFormation + * namespace) to its manifest service-code id. Case-insensitive, with a + * documented override table for exceptions (e.g. `StepFunctions → states`). + */ +export function mapLabelToServiceId(label: string): string { + const lower = label.toLowerCase(); + return LABEL_OVERRIDES[lower] ?? lower; +} diff --git a/src/platforms/aws/services/serviceProvider.ts b/src/platforms/aws/services/serviceProvider.ts new file mode 100644 index 0000000..a137065 --- /dev/null +++ b/src/platforms/aws/services/serviceProvider.ts @@ -0,0 +1,106 @@ +import { Stack } from "@aws-sdk/client-cloudformation"; +import type { StackResourceSummary } from "@aws-sdk/client-cloudformation"; + +import { InternalError } from "../../../utils/errors.ts"; +import type ARN from "../models/arnModel.ts"; + +/** + * Supported field types for resource descriptions. The type indicates + * what operations (hyperlinking etc) can be done with the data values + */ +export enum FieldType { + NAME = "name" /* name, status, type, or any other short ID */, + ARN = "arn" /* can be hyperlinked */, + DATE = "date", + SHORT_TEXT = "shortText", + LONG_TEXT = "longText" /* can be shown in an editor for easier reading */, + JSON = "json" /* can be shown in an editor with JSON syntax highlighting */, + NUMBER = "number" /* numeric value, e.g. count of resources */, + LOG_GROUP = "logGroup" /* can be hyperlinked to CloudWatch Logs */, +} + +/** + * Used in various places to unique identify a service/resource/ARN combination + */ +export type ServiceResourceArnTuple = { + serviceId: string; + resourceType: string; + arn: string; +}; + +/** + * Abstract parent class for all service providers. + */ +export abstract class ServiceProvider { + /** + * Map of the provider's resource types to their human-facing names [singular, plural]. + * Must be overidden by subclasses + */ + protected abstract resourceTypes: Record; + + /** + * Provide the ID of the AWS service managed by this provider + */ + abstract getId(): string; + + /** + * Return the human-readable name of this AWS service. + */ + abstract getName(): string; + + /** + * Return the ARNs associated with the resource type + */ + abstract getResourceArns( + profile: string, + region: string, + resourceType: string, + ): Promise; + + /** + * Return the fields associated with the resource, to appear in the Resource Details view + */ + abstract describeResource( + profile: string, + arn: ARN, + ): Promise<{ field: string; value: string; type: FieldType }[]>; + + /** + * Given a CloudFormation resource record, compute the corresponding ARN resource name + */ + abstract getArnResourceNameForCloudFormationResource( + stackResourceSummary: StackResourceSummary, + ): { resourceType: string; resourceName: string }; + + /** + * Return the resource type names [singular, plural] for this AWS service + */ + public getResourceTypeNames(resourceType: string): string[] { + const resourceTypeNames = this.resourceTypes[resourceType]; + if (!resourceTypeNames) { + throw new InternalError(`Unknown resource type: ${resourceType}`); + } + return resourceTypeNames; + } + + /** + * Get the resource types for this AWS service. + * @returns An array of resource type IDs. + */ + public getResourceTypes(): string[] { + return Object.keys(this.resourceTypes); + } + + /** + * Map of metamodel API-operation name -> resource type id, used to narrow the + * LocalStack "View: All Resources" focus to the resource types actually + * present (the metamodel records one list-operation key per present type). + * Empty by default: a single-type service needs no map (the metamodel focus + * falls back to its sole type), and providers that have not declared their + * operations fall back to their full type set. Multi-type providers SHOULD + * override (or, for declarative services, annotate each type's `metamodelOp`). + */ + public getMetamodelOperationMap(): Map { + return new Map(); + } +} diff --git a/src/plugins/app-inspector-webview.ts b/src/plugins/app-inspector-webview.ts index 0e55466..3ee8690 100644 --- a/src/plugins/app-inspector-webview.ts +++ b/src/plugins/app-inspector-webview.ts @@ -3,196 +3,77 @@ import path from "node:path"; import { commands, - window, - ViewColumn, - EventEmitter, - ThemeColor, - ThemeIcon, - TreeItem, - TreeItemCollapsibleState, - Uri, extensions, + Uri, + ViewColumn, version as vscodeVersion, + window, } from "vscode"; -import type { - ProviderResult, - TreeDataProvider, - Event, - WebviewPanel, -} from "vscode"; +import type { WebviewPanel } from "vscode"; import { createPlugin } from "../plugins.ts"; -import type { - LocalStackStatus, - LocalStackStatusTracker, -} from "../utils/localstack-status.ts"; - -export default createPlugin( - "app-inspector-webview", - ({ context, localStackStatusTracker }) => { - let appInspectorPanel: WebviewPanel | undefined; - context.subscriptions.push( - commands.registerCommand("localstack.openAppInspector", async () => { - if (appInspectorPanel) { - appInspectorPanel.reveal(); - return; - } - - const panel = window.createWebviewPanel( - "localStackAppInspector", - `App Inspector`, - ViewColumn.Active, - { - enableScripts: true, - retainContextWhenHidden: true, - }, - ); - appInspectorPanel = panel; - - panel.onDidDispose(() => { - appInspectorPanel = undefined; - }); - - const appInspectorDist = path.resolve( - import.meta.dirname, - "../resources/app-inspector/dist", - ); - const html = await readFile( - path.join(appInspectorDist, "index.html"), - "utf-8", - ); - const extensionVersion = - ( - extensions.getExtension("localstack.localstack")?.packageJSON as { - version?: string; - } - )?.version ?? "unknown"; - - panel.webview.html = html - .replaceAll(/"(\/.*?\.(?:js|css))"/g, (_, asset: string) => { - return JSON.stringify( - panel.webview - .asWebviewUri( - Uri.joinPath( - context.extensionUri, - "resources/app-inspector/dist", - asset, - ), - ) - .toString(), - ); - }) - .replace( - "window.__APP_INSPECTOR_CONTEXT__ = null;", - `window.__APP_INSPECTOR_CONTEXT__ = ${JSON.stringify({ - source: "vscode", - ideVersion: vscodeVersion, - extensionVersion, - })};`, - ); - }), - ); - - const provider = new InstancesTreeDataProvider({ - localStackStatusTracker, - }); - const instancesTreeView = window.createTreeView("localstack.instances", { - treeDataProvider: provider, - showCollapseAll: false, - }); - }, -); - -class InstancesTreeItem extends TreeItem { - children?: InstancesTreeItem[]; -} - -interface InstancesTreeDataProviderOptions { - localStackStatusTracker: LocalStackStatusTracker; -} - -class InstancesTreeDataProvider implements TreeDataProvider { - readonly #onDidChangeTreeData = new EventEmitter< - // biome-ignore lint/suspicious/noConfusingVoidType: void is required by Event - InstancesTreeItem | undefined | void - >(); - - // biome-ignore lint/suspicious/noConfusingVoidType: void is required by Event - readonly onDidChangeTreeData: Event = - this.#onDidChangeTreeData.event; - - #rootItems: InstancesTreeItem[] = []; - - constructor(options: InstancesTreeDataProviderOptions) { - const appInspectorItem = new InstancesTreeItem( - "App Inspector", - TreeItemCollapsibleState.None, - ); - appInspectorItem.description = "Click to open"; - appInspectorItem.command = { - title: "Open App Inspector", - command: "localstack.openAppInspector", - }; - - const instanceItem = new InstancesTreeItem( - "localhost.localstack.cloud:4566", - TreeItemCollapsibleState.Expanded, - ); - instanceItem.children = [ - (() => { - const item = new InstancesTreeItem( - "Status", - TreeItemCollapsibleState.None, - ); - options.localStackStatusTracker.onChange((status) => { - item.description = status; - this.#onDidChangeTreeData.fire(item); - }); - - return item; - })(), - appInspectorItem, - ]; - - this.#rootItems.push(instanceItem); - - options.localStackStatusTracker.onChange((status) => { - instanceItem.iconPath = getLocalStackStatusThemeIcon(status); - this.#onDidChangeTreeData.fire(instanceItem); - }); - } - - getChildren( - element?: InstancesTreeItem, - ): ProviderResult { - if (element) { - return element.children; - } - - return this.#rootItems; - } +export default createPlugin("app-inspector-webview", ({ context }) => { + let appInspectorPanel: WebviewPanel | undefined; + context.subscriptions.push( + commands.registerCommand("localstack.openAppInspector", async () => { + if (appInspectorPanel) { + appInspectorPanel.reveal(); + return; + } + + const panel = window.createWebviewPanel( + "localStackAppInspector", + `App Inspector`, + ViewColumn.Active, + { + enableScripts: true, + retainContextWhenHidden: true, + }, + ); + appInspectorPanel = panel; - getTreeItem(element: InstancesTreeItem): TreeItem { - return element; - } -} + panel.onDidDispose(() => { + appInspectorPanel = undefined; + }); -function getLocalStackStatusThemeIcon(status: LocalStackStatus): ThemeIcon { - switch (status) { - case "starting": - return new ThemeIcon("circle-outline"); - case "running": - return new ThemeIcon("circle-filled"); - case "stopping": - return new ThemeIcon( - "circle-filled", - new ThemeColor("disabledForeground"), + const appInspectorDist = path.resolve( + import.meta.dirname, + "../resources/app-inspector/dist", ); - case "stopped": - return new ThemeIcon( - "circle-outline", - new ThemeColor("disabledForeground"), + const html = await readFile( + path.join(appInspectorDist, "index.html"), + "utf-8", ); - } -} + const extensionVersion = + ( + extensions.getExtension("localstack.localstack")?.packageJSON as { + version?: string; + } + )?.version ?? "unknown"; + + panel.webview.html = html + .replaceAll(/"(\/.*?\.(?:js|css))"/g, (_, asset: string) => { + return JSON.stringify( + panel.webview + .asWebviewUri( + Uri.joinPath( + context.extensionUri, + "resources/app-inspector/dist", + asset, + ), + ) + .toString(), + ); + }) + .replace( + "window.__APP_INSPECTOR_CONTEXT__ = null;", + `window.__APP_INSPECTOR_CONTEXT__ = ${JSON.stringify({ + source: "vscode", + ideVersion: vscodeVersion, + extensionVersion, + })};`, + ); + }), + ); +}); diff --git a/src/plugins/manage.ts b/src/plugins/manage.ts index 126cd74..85acf51 100644 --- a/src/plugins/manage.ts +++ b/src/plugins/manage.ts @@ -13,7 +13,7 @@ export default createPlugin( context.subscriptions.push( commands.registerCommand("localstack.start", async () => { if (localStackStatusTracker.status() !== "stopped") { - window.showInformationMessage("LocalStack is already running."); + void window.showInformationMessage("LocalStack is already running."); return; } localStackStatusTracker.forceContainerStatus("running"); @@ -28,7 +28,7 @@ export default createPlugin( context.subscriptions.push( commands.registerCommand("localstack.stop", () => { if (localStackStatusTracker.status() !== "running") { - window.showInformationMessage("LocalStack is not running."); + void window.showInformationMessage("LocalStack is not running."); return; } localStackStatusTracker.forceContainerStatus("stopping"); diff --git a/src/plugins/resource-browser.ts b/src/plugins/resource-browser.ts new file mode 100644 index 0000000..38a6cc7 --- /dev/null +++ b/src/plugins/resource-browser.ts @@ -0,0 +1,95 @@ +import { commands, window, workspace } from "vscode"; + +import { ProviderFactory } from "../platforms/aws/services/providerFactory.ts"; +import { createPlugin } from "../plugins.ts"; +import { clearMemoizedCaches } from "../utils/memoize.ts"; +import { registerLocalStackCommands } from "../views/explore/commands.ts"; +import { LocalStackViewProvider } from "../views/explore/viewProvider.ts"; +import { ResourceDetailsViewProvider } from "../views/resource-details/viewProvider.ts"; +import { ResourceArnTreeItem } from "../views/resources/treeItems.ts"; +import { ResourceViewProvider } from "../views/resources/viewProvider.ts"; + +export default createPlugin( + "resource-browser", + ({ context, localStackStatusTracker, outputChannel }) => { + /* Service providers give access to AWS resource information. */ + ProviderFactory.initialize(); + + const localStackProvider = new LocalStackViewProvider( + localStackStatusTracker, + outputChannel, + ); + const resourcesProvider = new ResourceViewProvider(); + const detailsProvider = new ResourceDetailsViewProvider(); + + /* Resources is a tree view; Resource Details is a webview (table layout). */ + const resourcesView = window.createTreeView("localstack.resources", { + treeDataProvider: resourcesProvider, + }); + const detailsView = window.registerWebviewViewProvider( + "localstack.resourceDetails", + detailsProvider, + ); + context.subscriptions.push(resourcesView, detailsView); + + /* Selecting a resource updates the Resource Details view. */ + context.subscriptions.push( + resourcesView.onDidChangeSelection((e) => { + if ( + e.selection.length === 1 && + e.selection[0] instanceof ResourceArnTreeItem + ) { + const item = e.selection[0]; + const profile = item.parent.parent.parent.profile.id; + detailsProvider.setArn(profile, item.arn); + detailsProvider.reveal(); + } + }), + ); + + /* The LocalStack view is single-select: activating one focus selector + * deactivates any other. */ + const localStackView = window.createTreeView("localstack.instances", { + treeDataProvider: localStackProvider, + canSelectMany: false, + showCollapseAll: true, + }); + context.subscriptions.push( + localStackView, + localStackView.onDidChangeSelection((e) => { + /* Retain a producer (not just the computed focus) so the Resources + * view's refresh button can recompute it — re-querying the metamodel + * for the current selection. */ + const selection = e.selection; + void resourcesProvider.setFocusProducer(() => + localStackProvider.computeFocus(selection), + ); + }), + ); + + context.subscriptions.push( + commands.registerCommand("localstack.refreshResources", () => { + /* A manual refresh should re-query AWS, not replay cached account + * ids / region lists / clients — drop the caches first so newly + * created resources and re-authenticated credentials are picked up. */ + clearMemoizedCaches(); + return resourcesProvider.refresh(); + }), + commands.registerCommand("localstack.refreshResourceDetails", () => + detailsProvider.refresh(), + ), + ...registerLocalStackCommands(localStackProvider, () => { + void resourcesProvider.refresh(); + }), + ); + + /* Refresh the LocalStack view when its backing settings change. */ + context.subscriptions.push( + workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("localstack.cloudProfiles")) { + localStackProvider.refresh(); + } + }), + ); + }, +); diff --git a/src/plugins/setup.ts b/src/plugins/setup.ts index 6f92d12..57a8204 100644 --- a/src/plugins/setup.ts +++ b/src/plugins/setup.ts @@ -43,7 +43,7 @@ export default createPlugin( }, }); - window.withProgress( + void window.withProgress( { location: ProgressLocation.Notification, title: "Setup LocalStack", @@ -328,25 +328,25 @@ export default createPlugin( ///////////////////////////////////////////////////////////////////// if (localStackStatusTracker.status() === "running") { - window + void window .showInformationMessage("LocalStack is running.", { title: "View Logs", command: "localstack.viewLogs", }) .then((selection) => { if (selection) { - commands.executeCommand(selection.command); + void commands.executeCommand(selection.command); } }); } else { - window + void window .showInformationMessage("LocalStack is ready to start.", { title: "Start LocalStack", command: "localstack.start", }) .then((selection) => { if (selection) { - commands.executeCommand(selection.command); + void commands.executeCommand(selection.command); } }); } diff --git a/src/test/build/detailFieldGenerator.test.ts b/src/test/build/detailFieldGenerator.test.ts new file mode 100644 index 0000000..56df764 --- /dev/null +++ b/src/test/build/detailFieldGenerator.test.ts @@ -0,0 +1,135 @@ +/* The generator under test is an untyped .mjs (its exports type as `any`), so + * the type-aware "unsafe" rules fire on every call/result here only as noise. */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ +import assert from "node:assert"; + +import { + importanceRank, + mapFieldType, + rankAndSelect, + resolveOutputMembers, + toLabel, +} from "../../../build/generate-detail-fields.mjs"; + +/** + * The detail-field generator is a dev-time authoring aid: it turns an offline + * AWS API output shape into a ranked, capped, typed first-cut `detail` spec. + * These tests pin its pure logic — type mapping, the importance heuristic, + * label formatting, and model resolution — which is what determines the quality + * of the generated specs. + */ +type SelectedField = { label: string; path: string; typeName: string }; +suite("detail-field generator: mapFieldType", () => { + test("timestamps -> DATE, numerics -> NUMBER", () => { + assert.strictEqual(mapFieldType("CreatedAt", "timestamp"), "DATE"); + assert.strictEqual(mapFieldType("MemorySize", "integer"), "NUMBER"); + assert.strictEqual(mapFieldType("Weight", "double"), "NUMBER"); + }); + test("nested shapes -> JSON", () => { + assert.strictEqual(mapFieldType("Tags", "map"), "JSON"); + assert.strictEqual(mapFieldType("Subnets", "list"), "JSON"); + }); + test("string refinements: Arn -> ARN, LogGroup -> LOG_GROUP, else NAME", () => { + assert.strictEqual(mapFieldType("RoleArn", "string"), "ARN"); + assert.strictEqual(mapFieldType("LogGroupName", "string"), "LOG_GROUP"); + assert.strictEqual(mapFieldType("Description", "string"), "NAME"); + }); +}); + +suite("detail-field generator: importanceRank", () => { + test("orders identifiers above status above timestamps above scalars", () => { + assert.ok( + importanceRank("FunctionName", "string") < + importanceRank("State", "string"), + ); + assert.ok( + importanceRank("State", "string") < + importanceRank("LastModified", "timestamp"), + ); + assert.ok( + importanceRank("LastModified", "timestamp") < + importanceRank("MemorySize", "integer"), + ); + assert.ok( + importanceRank("MemorySize", "integer") < importanceRank("Tags", "map"), + ); + }); +}); + +suite("detail-field generator: toLabel", () => { + test("spaces camel/Pascal case", () => { + assert.strictEqual(toLabel("FunctionName"), "Function Name"); + assert.strictEqual(toLabel("creationDate"), "Creation Date"); + assert.strictEqual(toLabel("KMSKeyArn"), "KMS Key Arn"); + }); +}); + +suite("detail-field generator: rankAndSelect", () => { + test("excludes metadata/pagination/blobs, ranks, and caps", () => { + const members = [ + { name: "ResponseMetadata", shapeType: "structure" }, + { name: "NextToken", shapeType: "string" }, + { name: "Code", shapeType: "blob" }, + { name: "FunctionArn", shapeType: "string" }, + { name: "FunctionName", shapeType: "string" }, + { name: "State", shapeType: "string" }, + { name: "LastModified", shapeType: "timestamp" }, + { name: "MemorySize", shapeType: "integer" }, + { name: "Runtime", shapeType: "string" }, + { name: "Tags", shapeType: "map" }, + ]; + const selected = rankAndSelect(members); + + /* metadata, pagination token, and blob are dropped */ + assert.ok( + !selected.some((f: SelectedField) => f.path === "ResponseMetadata"), + ); + assert.ok(!selected.some((f: SelectedField) => f.path === "NextToken")); + assert.ok(!selected.some((f: SelectedField) => f.path === "Code")); + + assert.deepStrictEqual( + selected.map((f: SelectedField) => f.path), + [ + "FunctionArn", + "FunctionName", + "State", + "LastModified", + "MemorySize", + "Runtime", + "Tags", + ], + ); + assert.deepStrictEqual( + selected.map((f: SelectedField) => f.typeName), + ["ARN", "NAME", "NAME", "DATE", "NUMBER", "NAME", "JSON"], + ); + }); + + test("respects the field cap", () => { + const members = Array.from({ length: 30 }, (_, i) => ({ + name: `Field${i}`, + shapeType: "string", + })); + assert.strictEqual(rankAndSelect(members, 12).length, 12); + }); +}); + +suite("detail-field generator: resolveOutputMembers", () => { + test("resolves an operation output shape to member name + type", () => { + const model = { + operations: { GetThing: { output: { shape: "GetThingOutput" } } }, + shapes: { + GetThingOutput: { + type: "structure", + members: { Name: { shape: "S" }, Count: { shape: "I" } }, + }, + S: { type: "string" }, + I: { type: "integer" }, + }, + }; + assert.deepStrictEqual(resolveOutputMembers(model, "GetThing"), [ + { name: "Name", shapeType: "string" }, + { name: "Count", shapeType: "integer" }, + ]); + }); +}); diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 3ecf628..15a1cef 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -8,7 +8,7 @@ import { window } from "vscode"; // import * as myExtension from '../../extension'; suite("Extension Test Suite", () => { - window.showInformationMessage("Start all tests."); + void window.showInformationMessage("Start all tests."); test("Sample test", () => { assert.strictEqual(-1, [1, 2, 3].indexOf(5)); diff --git a/src/test/memoize.test.ts b/src/test/memoize.test.ts new file mode 100644 index 0000000..edcc355 --- /dev/null +++ b/src/test/memoize.test.ts @@ -0,0 +1,75 @@ +import * as assert from "node:assert"; + +import { clearMemoizedCaches, memoize } from "../utils/memoize.ts"; + +suite("memoize", () => { + test("caches by argument key", () => { + let calls = 0; + const m = memoize((x: number) => { + calls++; + return x * 2; + }); + assert.strictEqual(m(2), 4); + assert.strictEqual(m(2), 4); + assert.strictEqual(calls, 1); + assert.strictEqual(m(3), 6); + assert.strictEqual(calls, 2); + }); + + test("dedupes concurrent in-flight async callers into one invocation", async () => { + let calls = 0; + const m = memoize((key: string) => { + calls++; + return Promise.resolve(`${key}!`); + }); + + /* Both calls happen before the first resolves; with promise caching they + * share the single in-flight request rather than each invoking `func`. */ + const [a, b] = await Promise.all([m("x"), m("x")]); + assert.strictEqual(a, "x!"); + assert.strictEqual(b, "x!"); + assert.strictEqual(calls, 1); + }); + + test("does not cache a rejected promise; the next call retries", async () => { + let attempts = 0; + const m = memoize((key: string) => { + attempts++; + return attempts === 1 + ? Promise.reject(new Error("transient")) + : Promise.resolve(`${key}-ok`); + }); + + await assert.rejects(m("x"), /transient/); + /* The failure was evicted, so the retry re-invokes and succeeds. */ + assert.strictEqual(await m("x"), "x-ok"); + assert.strictEqual(attempts, 2); + }); + + test("clear() drops a single function's cache", () => { + let calls = 0; + const m = memoize((x: number) => { + calls++; + return x; + }); + m(1); + m(1); + assert.strictEqual(calls, 1); + m.clear(); + m(1); + assert.strictEqual(calls, 2); + }); + + test("clearMemoizedCaches() drops registered caches", () => { + let calls = 0; + const m = memoize((x: number) => { + calls++; + return x; + }); + m(1); + assert.strictEqual(calls, 1); + clearMemoizedCaches(); + m(1); + assert.strictEqual(calls, 2); + }); +}); diff --git a/src/test/models/arnModel.test.ts b/src/test/models/arnModel.test.ts new file mode 100644 index 0000000..9abd77f --- /dev/null +++ b/src/test/models/arnModel.test.ts @@ -0,0 +1,70 @@ +import assert from "node:assert"; + +import ARN from "../../platforms/aws/models/arnModel.ts"; +import { InternalError } from "../../utils/errors.ts"; + +suite("ARN", () => { + test("parses a valid Step Functions ARN", () => { + const arn = new ARN( + "arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine", + ); + + assert.strictEqual(arn.partition, "aws"); + assert.strictEqual(arn.service, "states"); + assert.strictEqual(arn.region, "us-east-1"); + assert.strictEqual(arn.accountId, "123456789012"); + assert.strictEqual(arn.resourceId, "stateMachine:my-state-machine"); + assert.strictEqual(arn.resourceType, "stateMachine"); + assert.strictEqual(arn.resourceName, "my-state-machine"); + }); + + test("parses a valid DynamoDB ARN", () => { + const arn = new ARN( + "arn:aws:dynamodb:ap-southeast-2:123456789012:table/my-table", + ); + + assert.strictEqual(arn.service, "dynamodb"); + assert.strictEqual(arn.resourceType, "table"); + assert.strictEqual(arn.resourceName, "my-table"); + }); + + test("parses a valid Lambda ARN", () => { + const arn = new ARN( + "arn:aws:lambda:eu-central-1:123456789012:function:my-function", + ); + + assert.strictEqual(arn.service, "lambda"); + assert.strictEqual(arn.resourceType, "function"); + assert.strictEqual(arn.resourceName, "my-function"); + }); + + test("parses a valid SNS ARN without a resource type", () => { + const arn = new ARN("arn:aws:sns:us-east-1:123456789012:my-topic"); + + assert.strictEqual(arn.service, "sns"); + assert.strictEqual(arn.resourceId, "my-topic"); + assert.strictEqual(arn.resourceType, undefined); + assert.strictEqual(arn.resourceName, "my-topic"); + }); + + test("accepts valid non-standard partition names", () => { + const arn = new ARN( + "arn:aws-us-gov:lambda:us-east-1:123456789012:function:my-function", + ); + + assert.strictEqual(arn.partition, "aws-us-gov"); + }); + + test("rejects invalid ARNs", () => { + assert.throws( + () => + new ARN("aws:lambda:eu-central-1:123456789012:function:my-function"), + InternalError, + ); + assert.throws( + () => new ARN("arn:aws:lambda:eu-central-1:12347:function:my-function"), + InternalError, + ); + assert.throws(() => new ARN("completely-invalid-arn"), InternalError); + }); +}); diff --git a/src/test/models/awsConfig.test.ts b/src/test/models/awsConfig.test.ts new file mode 100644 index 0000000..47ed973 --- /dev/null +++ b/src/test/models/awsConfig.test.ts @@ -0,0 +1,131 @@ +import assert from "node:assert"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import AWSConfig from "../../platforms/aws/models/awsConfig.ts"; + +/** + * AWSConfig reads a hardcoded `~/.aws/config` path via a private static field. + * The tests repoint that field at a temporary fixture file (and control the + * `AWS_REGION` env var) so the parsing logic can be exercised deterministically + * without touching the developer's real AWS configuration. + */ +const configHandle = AWSConfig as unknown as { AWS_CONFIG_FILE: string }; + +const CONFIG_CONTENTS = `[default] +region = us-east-1 +endpoint_url = http://localhost:4566 + +[profile staging] +region = eu-west-1 +`; + +suite("AWSConfig", () => { + let tempDir: string; + let configPath: string; + let originalConfigFile: string; + let originalAwsRegion: string | undefined; + + suiteSetup(() => { + originalConfigFile = configHandle.AWS_CONFIG_FILE; + originalAwsRegion = process.env.AWS_REGION; + delete process.env.AWS_REGION; + + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "awsconfig-test-")); + configPath = path.join(tempDir, "config"); + fs.writeFileSync(configPath, CONFIG_CONTENTS, "utf-8"); + configHandle.AWS_CONFIG_FILE = configPath; + }); + + suiteTeardown(() => { + configHandle.AWS_CONFIG_FILE = originalConfigFile; + if (originalAwsRegion === undefined) { + delete process.env.AWS_REGION; + } else { + process.env.AWS_REGION = originalAwsRegion; + } + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + teardown(() => { + /* Ensure tests that set the env var don't leak into the next test. */ + delete process.env.AWS_REGION; + }); + + test("getProfileNames lists the default profile and named profiles", () => { + assert.deepStrictEqual(AWSConfig.getProfileNames(), ["default", "staging"]); + }); + + test("getRegionForProfile reads the region from the config file", () => { + assert.strictEqual(AWSConfig.getRegionForProfile("default"), "us-east-1"); + assert.strictEqual(AWSConfig.getRegionForProfile("staging"), "eu-west-1"); + }); + + test("getRegionForProfile lets AWS_REGION take precedence", () => { + process.env.AWS_REGION = "ap-southeast-2"; + assert.strictEqual( + AWSConfig.getRegionForProfile("default"), + "ap-southeast-2", + ); + assert.strictEqual( + AWSConfig.getRegionForProfile("staging"), + "ap-southeast-2", + ); + }); + + test("getClientConfig surfaces the profile's endpoint_url when present", () => { + assert.deepStrictEqual(AWSConfig.getClientConfig("default", "us-east-1"), { + profile: "default", + region: "us-east-1", + endpoint: "http://localhost:4566", + }); + }); + + test("getClientConfig leaves the endpoint undefined when not configured", () => { + assert.deepStrictEqual(AWSConfig.getClientConfig("staging", "eu-west-1"), { + profile: "staging", + region: "eu-west-1", + endpoint: undefined, + }); + }); + + test("getClientConfig falls back to the profile's region when none is given", () => { + /* A region-less ARN (e.g. S3) passes no region; use the profile default. */ + assert.deepStrictEqual(AWSConfig.getClientConfig("staging"), { + profile: "staging", + region: "eu-west-1", + endpoint: undefined, + }); + }); + + test("getClientConfig falls back to us-east-1 when the profile has no region", () => { + assert.deepStrictEqual(AWSConfig.getClientConfig("unknown-profile"), { + profile: "unknown-profile", + region: "us-east-1", + endpoint: undefined, + }); + }); + + test("getEndpointForProfile returns the endpoint_url when present", () => { + assert.strictEqual( + AWSConfig.getEndpointForProfile("default"), + "http://localhost:4566", + ); + }); + + test("getEndpointForProfile returns undefined when not configured", () => { + assert.strictEqual(AWSConfig.getEndpointForProfile("staging"), undefined); + }); + + test("returns safe empty values when the config file is missing", () => { + const previous = configHandle.AWS_CONFIG_FILE; + configHandle.AWS_CONFIG_FILE = path.join(tempDir, "does-not-exist"); + try { + assert.deepStrictEqual(AWSConfig.getProfileNames(), []); + assert.strictEqual(AWSConfig.getRegionForProfile("default"), undefined); + } finally { + configHandle.AWS_CONFIG_FILE = previous; + } + }); +}); diff --git a/src/test/models/cfnStackModel.test.ts b/src/test/models/cfnStackModel.test.ts new file mode 100644 index 0000000..1ddaec0 --- /dev/null +++ b/src/test/models/cfnStackModel.test.ts @@ -0,0 +1,239 @@ +import assert from "node:assert"; + +import type { StackResourceSummary } from "@aws-sdk/client-cloudformation"; +import type { LogOutputChannel } from "vscode"; + +import { CloudFormation } from "../../platforms/aws/clients/cloudformation.ts"; +import ARN from "../../platforms/aws/models/arnModel.ts"; +import CfnStackModel from "../../platforms/aws/models/cfnStackModel.ts"; +import { ProviderFactory } from "../../platforms/aws/services/providerFactory.ts"; + +/** + * CfnStackModel converts a CloudFormation stack's resources into a Focus model. + * It depends on two collaborators we stub here: + * - `ProviderFactory`, initialized with a bare context (the service providers + * only touch the context for icon paths, which this conversion never needs); + * - `CloudFormation.listStackResources`, whose static method is swapped for a + * stub returning a fixed resource list so no AWS calls are made. + */ +const cfnHandle = CloudFormation as unknown as { + listStackResources: ( + profile: string, + arn: ARN, + ) => Promise; +}; + +const STACK_ARN = + "arn:aws:cloudformation:us-east-1:000000000000:stack/my-stack/abc-123"; + +/** + * Build a StackResourceSummary, filling the fields the SDK type requires but + * the conversion ignores (status/timestamp) so tests only specify what matters. + */ +function res(partial: Partial): StackResourceSummary { + return { + ResourceStatus: "CREATE_COMPLETE", + LastUpdatedTimestamp: new Date(0), + ...partial, + } as StackResourceSummary; +} + +/** Build a LogOutputChannel stub that records the messages passed to `warn`. */ +function makeLogStub(): { log: LogOutputChannel; warnings: string[] } { + const warnings: string[] = []; + const log = { + warn: (message: string) => { + warnings.push(message); + }, + } as unknown as LogOutputChannel; + return { log, warnings }; +} + +suite("CfnStackModel", () => { + let originalListStackResources: typeof cfnHandle.listStackResources; + + suiteSetup(() => { + ProviderFactory.initialize(); + originalListStackResources = cfnHandle.listStackResources; + }); + + suiteTeardown(() => { + cfnHandle.listStackResources = originalListStackResources; + }); + + function stubResources(resources: Partial[]) { + const full = resources.map(res); + cfnHandle.listStackResources = () => Promise.resolve(full); + } + + test("maps supported resources into a grouped Focus", async () => { + stubResources([ + { + LogicalResourceId: "MyQueue", + ResourceType: "AWS::SQS::Queue", + PhysicalResourceId: + "https://sqs.us-east-1.amazonaws.com/000000000000/my-queue", + }, + { + LogicalResourceId: "MyFunction", + ResourceType: "AWS::Lambda::Function", + PhysicalResourceId: "my-function", + }, + ]); + + const { log, warnings } = makeLogStub(); + const focus = await new CfnStackModel( + "default", + new ARN(STACK_ARN), + log, + ).toFocusModel(); + + assert.strictEqual(warnings.length, 0); + assert.strictEqual(focus.profiles.length, 1); + assert.strictEqual(focus.profiles[0].id, "default"); + + const region = focus.profiles[0].regions[0]; + assert.strictEqual(region.id, "us-east-1"); + + const sqs = region.services.find((s) => s.id === "sqs"); + assert.ok(sqs, "expected an sqs service entry"); + assert.deepStrictEqual(sqs.resourcetypes, [ + { id: "queue", arns: ["arn:aws:sqs:us-east-1:000000000000:my-queue"] }, + ]); + + const lambda = region.services.find((s) => s.id === "lambda"); + assert.ok(lambda, "expected a lambda service entry"); + assert.deepStrictEqual(lambda.resourcetypes, [ + { + id: "function", + arns: ["arn:aws:lambda:us-east-1:000000000000:function:my-function"], + }, + ]); + }); + + test("groups multiple resources of the same type under one service", async () => { + stubResources([ + { + LogicalResourceId: "QueueA", + ResourceType: "AWS::SQS::Queue", + PhysicalResourceId: + "https://sqs.us-east-1.amazonaws.com/000000000000/queue-a", + }, + { + LogicalResourceId: "QueueB", + ResourceType: "AWS::SQS::Queue", + PhysicalResourceId: + "https://sqs.us-east-1.amazonaws.com/000000000000/queue-b", + }, + ]); + + const { log } = makeLogStub(); + const focus = await new CfnStackModel( + "default", + new ARN(STACK_ARN), + log, + ).toFocusModel(); + + const sqs = focus.profiles[0].regions[0].services.find( + (s) => s.id === "sqs", + ); + assert.ok(sqs); + assert.deepStrictEqual(sqs.resourcetypes[0].arns, [ + "arn:aws:sqs:us-east-1:000000000000:queue-a", + "arn:aws:sqs:us-east-1:000000000000:queue-b", + ]); + }); + + test("skips and warns about resources it cannot map, keeping the rest", async () => { + stubResources([ + { + LogicalResourceId: "MyQueue", + ResourceType: "AWS::SQS::Queue", + PhysicalResourceId: + "https://sqs.us-east-1.amazonaws.com/000000000000/my-queue", + }, + /* Unsupported service: no provider is registered for "ec2". */ + { + LogicalResourceId: "MyInstance", + ResourceType: "AWS::EC2::Instance", + PhysicalResourceId: "i-1234567890abcdef0", + }, + /* Malformed summary: a missing ResourceType used to throw a + * TypeError ("Cannot read properties of undefined (reading 'split')") + * that aborted the whole stack. It must now be skipped instead. */ + { LogicalResourceId: "Mystery", PhysicalResourceId: "whatever" }, + ]); + + const { log, warnings } = makeLogStub(); + const focus = await new CfnStackModel( + "default", + new ARN(STACK_ARN), + log, + ).toFocusModel(); + + /* The recognized SQS queue still made it through. */ + const services = focus.profiles[0].regions[0].services; + assert.deepStrictEqual( + services.map((s) => s.id), + ["sqs"], + ); + + /* Both unmappable resources were reported, identified by logical id. */ + assert.strictEqual(warnings.length, 2); + assert.ok(warnings.some((w) => w.includes("MyInstance"))); + assert.ok(warnings.some((w) => w.includes("Mystery"))); + }); + + test("routes a StepFunctions CFN namespace to the states provider via label mapping", async () => { + stubResources([ + { + LogicalResourceId: "MyStateMachine", + ResourceType: "AWS::StepFunctions::StateMachine", + PhysicalResourceId: "my-state-machine", + }, + ]); + + const { log, warnings } = makeLogStub(); + const focus = await new CfnStackModel( + "default", + new ARN(STACK_ARN), + log, + ).toFocusModel(); + + /* The `states` provider does not yet implement CloudFormation mapping, so + * the resource is skipped — but the warning originates from the states + * provider, proving the label mapping routed `StepFunctions` to `states` + * rather than failing earlier at provider lookup. */ + assert.deepStrictEqual(focus.profiles[0].regions[0].services, []); + assert.strictEqual(warnings.length, 1); + assert.ok(warnings[0].includes("Step Functions")); + }); + + test("does not require a log channel", async () => { + stubResources([ + { + LogicalResourceId: "MyInstance", + ResourceType: "AWS::EC2::Instance", + PhysicalResourceId: "i-1234567890abcdef0", + }, + ]); + + /* No log passed: the skip path must not throw on the optional channel. */ + const focus = await new CfnStackModel( + "default", + new ARN(STACK_ARN), + ).toFocusModel(); + + assert.deepStrictEqual(focus.profiles[0].regions[0].services, []); + }); + + test("rejects ARNs that are not CloudFormation stacks", () => { + assert.throws( + () => + new CfnStackModel( + "default", + new ARN("arn:aws:sqs:us-east-1:000000000000:my-queue"), + ), + ); + }); +}); diff --git a/src/test/models/focus.test.ts b/src/test/models/focus.test.ts new file mode 100644 index 0000000..a645fd6 --- /dev/null +++ b/src/test/models/focus.test.ts @@ -0,0 +1,13 @@ +import assert from "node:assert"; + +import { makeWildcardFocus } from "../../models/focus.ts"; + +suite("Focus model", () => { + test("makeWildcardFocus builds a single profile/region wildcard focus", () => { + const focus = makeWildcardFocus("default", "us-east-1"); + assert.strictEqual(focus.profiles.length, 1); + assert.strictEqual(focus.profiles[0].id, "default"); + assert.strictEqual(focus.profiles[0].regions[0].id, "us-east-1"); + assert.strictEqual(focus.profiles[0].regions[0].services[0].id, "*"); + }); +}); diff --git a/src/test/models/metamodelFocus.test.ts b/src/test/models/metamodelFocus.test.ts new file mode 100644 index 0000000..d98d805 --- /dev/null +++ b/src/test/models/metamodelFocus.test.ts @@ -0,0 +1,149 @@ +import assert from "node:assert"; + +import { + metamodelToFocus, + parseMetamodel, +} from "../../platforms/aws/models/metamodelFocus.ts"; + +suite("metamodel -> Focus", () => { + test("parseMetamodel tolerates raw control characters", () => { + const withControlChar = `{"a": "x${String.fromCharCode(1)}y"}`; + assert.throws(() => JSON.parse(withControlChar)); + const parsed = parseMetamodel(withControlChar); + assert.strictEqual(parsed.a, "xy"); + }); + + test("maps supported services, drops unsupported, dedups global region", () => { + const payload = { + "000000000000": { + EventBridge: { "us-east-1": { listEventBuses: {} } }, + S3: { "us-east-1": { listBuckets: {} } }, + IAM: { "us-east-1": { listRoles: {} }, "": { listRoles: {} } }, + SSM: { "us-east-1": { describeParameters: {} } }, + CloudFormation: { "us-east-1": { listStacks: {} } }, + }, + }; + const resourceTypes = new Map([ + ["cloudformation", ["stack"]], + ["iam", ["role"]], + ]); + + /* Single-type services with no operation map fall back to their sole type. */ + const focus = metamodelToFocus(payload, resourceTypes, new Map()); + + assert.strictEqual(focus.profiles.length, 1); + assert.strictEqual(focus.profiles[0].id, "localstack"); + + /* The empty-string global mirror must not produce a region node. */ + const regions = focus.profiles[0].regions; + assert.strictEqual(regions.length, 1); + assert.strictEqual(regions[0].id, "us-east-1"); + + const serviceIds = regions[0].services.map((s) => s.id).sort(); + assert.deepStrictEqual(serviceIds, ["cloudformation", "iam"]); + + /* Resource types are expanded with wildcard ARNs. */ + const iam = regions[0].services.find((s) => s.id === "iam"); + assert.ok(iam); + assert.deepStrictEqual(iam.resourcetypes, [{ id: "role", arns: ["*"] }]); + }); + + test("applies the StepFunctions -> states label override", () => { + const payload = { + "000000000000": { + StepFunctions: { "us-east-1": { listStateMachines: {} } }, + }, + }; + /* The provider registers under the AWS service code `states`, not the + * metamodel label `StepFunctions`; the shared label mapping must bridge them. */ + const resourceTypes = new Map([ + ["states", ["statemachine"]], + ]); + + const focus = metamodelToFocus(payload, resourceTypes, new Map()); + + const serviceIds = focus.profiles[0].regions[0].services.map((s) => s.id); + assert.deepStrictEqual(serviceIds, ["states"]); + }); + + test("returns an empty focus when the default account is absent", () => { + const focus = metamodelToFocus( + { "999999999999": {} }, + new Map(), + new Map(), + ); + assert.strictEqual(focus.profiles[0].id, "localstack"); + assert.strictEqual(focus.profiles[0].regions.length, 0); + }); + + test("names only the resource types whose metamodel operation is present", () => { + /* SSM has five types but only a Parameter is deployed: the focus must list + * Parameters alone, not the other four types. */ + const payload = { + "000000000000": { + SSM: { "us-east-1": { describeParameters: {} } }, + }, + }; + const resourceTypes = new Map([ + [ + "ssm", + [ + "parameter", + "document", + "maintenancewindow", + "association", + "patchbaseline", + ], + ], + ]); + const operationMaps = new Map>([ + [ + "ssm", + new Map([ + ["describeParameters", "parameter"], + ["listDocuments", "document"], + ["describeMaintenanceWindows", "maintenancewindow"], + ["listAssociations", "association"], + ["describePatchBaselines", "patchbaseline"], + ]), + ], + ]); + + const focus = metamodelToFocus(payload, resourceTypes, operationMaps); + + const ssm = focus.profiles[0].regions[0].services.find( + (s) => s.id === "ssm", + ); + assert.ok(ssm); + assert.deepStrictEqual(ssm.resourcetypes, [ + { id: "parameter", arns: ["*"] }, + ]); + }); + + test("falls back to the full type set when an operation is unmapped", () => { + /* A present operation that maps to no known type must not hide resources: + * the service falls back to listing all its types. */ + const payload = { + "000000000000": { + SSM: { "us-east-1": { someUnknownOperation: {} } }, + }, + }; + const resourceTypes = new Map([ + ["ssm", ["parameter", "document"]], + ]); + const operationMaps = new Map>([ + ["ssm", new Map([["describeParameters", "parameter"]])], + ]); + + const focus = metamodelToFocus(payload, resourceTypes, operationMaps); + + const ssm = focus.profiles[0].regions[0].services.find( + (s) => s.id === "ssm", + ); + assert.ok(ssm); + assert.deepStrictEqual(ssm.resourcetypes.map((rt) => rt.id).sort(), [ + "document", + "parameter", + ]); + }); +}); diff --git a/src/test/models/regionModel.test.ts b/src/test/models/regionModel.test.ts new file mode 100644 index 0000000..73015a2 --- /dev/null +++ b/src/test/models/regionModel.test.ts @@ -0,0 +1,29 @@ +import assert from "node:assert"; + +import { + getAllRegionCodes, + getRegionLongName, +} from "../../platforms/aws/models/regionModel.ts"; + +/** + * Region lookups back both the cloud-profile add-region picker and the region + * labels shown in the Resources view. The latter renders whatever region the + * running emulator reports in its metamodel, so an unknown region must degrade + * to its code rather than throw and break the whole "All Resources" view. + */ +suite("regionModel", () => { + test("returns the long name for a known region", () => { + assert.strictEqual(getRegionLongName("us-east-1"), "US East (N. Virginia)"); + assert.strictEqual(getRegionLongName("cn-north-1"), "China (Beijing)"); + }); + + test("falls back to the region code for an unknown region", () => { + assert.strictEqual(getRegionLongName("xx-unknown-9"), "xx-unknown-9"); + }); + + test("exposes the China partition regions in the region list", () => { + const codes = getAllRegionCodes(); + assert.ok(codes.includes("cn-north-1")); + assert.ok(codes.includes("cn-northwest-1")); + }); +}); diff --git a/src/test/models/serviceManifest.test.ts b/src/test/models/serviceManifest.test.ts new file mode 100644 index 0000000..2ba45a9 --- /dev/null +++ b/src/test/models/serviceManifest.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert"; + +import { + getAllServiceIds, + getEntry, + getManifest, + mapLabelToServiceId, +} from "../../platforms/aws/services/serviceManifest.ts"; + +/** + * The service manifest is the static, generated source of truth for which AWS + * services the resource browser knows about. These tests pin its shape, a few + * representative entries (including the `stepfunctions → states` service-code + * remap and the hyphenated `cognito-idp` id), and the label-mapping behavior + * shared by the metamodel and CloudFormation paths. + */ +suite("serviceManifest", () => { + test("manifest is a non-empty list of {id, name} entries", () => { + const manifest = getManifest(); + assert.ok(manifest.length > 0, "expected a non-empty manifest"); + for (const entry of manifest) { + assert.strictEqual(typeof entry.id, "string"); + assert.ok(entry.id.length > 0); + assert.strictEqual(typeof entry.name, "string"); + assert.ok(entry.name.length > 0); + } + }); + + test("service ids are unique", () => { + const ids = getAllServiceIds(); + assert.strictEqual(new Set(ids).size, ids.length); + }); + + test("includes representative services under their AWS service codes", () => { + const ids = new Set(getAllServiceIds()); + for (const id of ["s3", "states", "cognito-idp", "logs", "events"]) { + assert.ok(ids.has(id), `expected manifest to contain ${id}`); + } + }); + + test("Step Functions is keyed by its AWS service code, not its coverage slug", () => { + assert.ok(getEntry("states"), "expected a `states` entry"); + assert.strictEqual( + getEntry("stepfunctions"), + undefined, + "coverage slug `stepfunctions` must not be a manifest id", + ); + assert.strictEqual(getEntry("states")?.name, "Step Functions"); + }); + + test("getEntry returns the entry for a known id and undefined otherwise", () => { + const s3 = getEntry("s3"); + assert.ok(s3); + assert.strictEqual(s3.id, "s3"); + assert.strictEqual(getEntry("not-a-real-service"), undefined); + }); + + suite("mapLabelToServiceId", () => { + test("lowercases simple PascalCase labels", () => { + assert.strictEqual( + mapLabelToServiceId("CloudFormation"), + "cloudformation", + ); + assert.strictEqual(mapLabelToServiceId("S3"), "s3"); + assert.strictEqual(mapLabelToServiceId("IAM"), "iam"); + }); + + test("applies the Step Functions override", () => { + assert.strictEqual(mapLabelToServiceId("StepFunctions"), "states"); + assert.strictEqual(mapLabelToServiceId("stepfunctions"), "states"); + }); + + test("preserves hyphenated ids such as cognito-idp", () => { + assert.strictEqual(mapLabelToServiceId("cognito-idp"), "cognito-idp"); + }); + }); +}); diff --git a/src/test/resources/instanceViewFocus.test.ts b/src/test/resources/instanceViewFocus.test.ts new file mode 100644 index 0000000..58610f7 --- /dev/null +++ b/src/test/resources/instanceViewFocus.test.ts @@ -0,0 +1,63 @@ +import assert from "node:assert"; + +import type { Focus } from "../../models/focus.ts"; +import { intersectMetamodelWithPairs } from "../../views/explore/viewProvider.ts"; + +/** + * An instance view's focus is the live metamodel focus narrowed to the view's + * chosen service/resource-type pairs: pairs not present in the metamodel are + * dropped, and empty services/regions are pruned. + */ +suite("intersectMetamodelWithPairs", () => { + const metamodel: Focus = { + version: "1.0", + profiles: [ + { + id: "localstack", + regions: [ + { + id: "us-east-1", + services: [ + { id: "ssm", resourcetypes: [{ id: "parameter", arns: ["*"] }] }, + { id: "sqs", resourcetypes: [{ id: "queue", arns: ["*"] }] }, + ], + }, + ], + }, + ], + }; + + test("keeps only the chosen pairs present in the metamodel", () => { + const focus = intersectMetamodelWithPairs(metamodel, [ + { service: "ssm", resourceType: "parameter" }, + ]); + const services = focus.profiles[0].regions[0].services; + assert.deepStrictEqual( + services.map((s) => s.id), + ["ssm"], + ); + assert.deepStrictEqual(services[0].resourcetypes, [ + { id: "parameter", arns: ["*"] }, + ]); + }); + + test("drops chosen pairs that are not deployed in the metamodel", () => { + /* SSM document is chosen but only a parameter is deployed. */ + const focus = intersectMetamodelWithPairs(metamodel, [ + { service: "ssm", resourceType: "document" }, + ]); + assert.deepStrictEqual(focus.profiles[0].regions, []); + }); + + test("prunes services and regions left empty", () => { + const focus = intersectMetamodelWithPairs(metamodel, [ + { service: "sqs", resourceType: "queue" }, + ]); + const services = focus.profiles[0].regions[0].services; + assert.deepStrictEqual( + services.map((s) => s.id), + ["sqs"], + ); + assert.strictEqual(focus.profiles[0].id, "localstack"); + }); +}); diff --git a/src/test/resources/metamodel-sample.json b/src/test/resources/metamodel-sample.json new file mode 100644 index 0000000..4a4504a --- /dev/null +++ b/src/test/resources/metamodel-sample.json @@ -0,0 +1,304 @@ +{ + "000000000000": { + "EventBridge": { + "us-east-1": { + "listEventBuses": { + "EventBuses": [ + { + "Name": "default", + "Arn": "arn:aws:events:us-east-1:000000000000:event-bus/default", + "CreationTime": "2026-06-21T18:04:47.863Z", + "LastModifiedTime": "2026-06-21T18:04:47.863Z" + } + ] + } + } + }, + "S3": { + "us-east-1": { + "listBuckets": { + "Buckets": [ + { + "Name": "cdk-hnb659fds-assets-000000000000-us-east-1", + "CreationDate": "2026-06-21T18:04:48.000Z", + "BucketRegion": "us-east-1", + "BucketArn": "arn:aws:s3:::cdk-hnb659fds-assets-000000000000-us-east-1" + } + ], + "Owner": { + "ID": "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a" + } + } + } + }, + "IAM": { + "us-east-1": { + "listRoles": { + "Roles": [ + { + "Path": "/", + "RoleName": "cdk-hnb659fds-cfn-exec-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAD6AABTQKO", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-cfn-exec-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.038Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { "Service": "cloudformation.amazonaws.com" } + } + ], + "Version": "2012-10-17" + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-deploy-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAIBIRR5HWV", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-deploy-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.666Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-file-publishing-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAHZZBLFSX4", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-file-publishing-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.674Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-image-publishing-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAA5GBH3MK4", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-image-publishing-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.678Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-lookup-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAJF4WBPPSG", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-lookup-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.683Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + } + ], + "IsTruncated": false + } + }, + "": { + "listRoles": { + "Roles": [ + { + "Path": "/", + "RoleName": "cdk-hnb659fds-cfn-exec-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAD6AABTQKO", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-cfn-exec-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.038Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { "Service": "cloudformation.amazonaws.com" } + } + ], + "Version": "2012-10-17" + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-deploy-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAIBIRR5HWV", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-deploy-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.666Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-file-publishing-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAHZZBLFSX4", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-file-publishing-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.674Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-image-publishing-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAA5GBH3MK4", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-image-publishing-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.678Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + }, + { + "Path": "/", + "RoleName": "cdk-hnb659fds-lookup-role-000000000000-us-east-1", + "RoleId": "AROAQAAAAAAAJF4WBPPSG", + "Arn": "arn:aws:iam::000000000000:role/cdk-hnb659fds-lookup-role-000000000000-us-east-1", + "CreateDate": "2026-06-21T18:04:48.683Z", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { "Null": { "sts:ExternalId": "true" } }, + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + }, + { + "Action": "sts:TagSession", + "Effect": "Allow", + "Principal": { "AWS": "000000000000" } + } + ] + }, + "MaxSessionDuration": 3600 + } + ], + "IsTruncated": false + } + } + }, + "SSM": { + "us-east-1": { + "describeParameters": { + "Parameters": [ + { + "Name": "/cdk-bootstrap/hnb659fds/version", + "Type": "String", + "LastModifiedDate": "2026-06-21T18:04:47.546Z", + "LastModifiedUser": "N/A", + "Version": 1, + "Tier": "Standard", + "DataType": "text" + } + ] + } + } + }, + "CloudFormation": { + "us-east-1": { + "listStacks": { + "StackSummaries": [ + { + "StackId": "arn:aws:cloudformation:us-east-1:000000000000:stack/CDKToolkit/a3f70502-8248-49ea-b7e1-df8f6dea5a35", + "StackName": "CDKToolkit", + "CreationTime": "2026-06-21T18:04:47.226Z", + "LastUpdatedTime": "2026-06-21T18:04:47.226Z", + "StackStatus": "CREATE_COMPLETE", + "DriftInformation": { "StackDriftStatus": "NOT_CHECKED" } + } + ] + } + } + } + } +} diff --git a/src/test/resources/regionViewFocus.test.ts b/src/test/resources/regionViewFocus.test.ts new file mode 100644 index 0000000..4994cd6 --- /dev/null +++ b/src/test/resources/regionViewFocus.test.ts @@ -0,0 +1,58 @@ +import assert from "node:assert"; + +import type { SavedView } from "../../views/explore/settings.ts"; +import { resolveRegionViewFocus } from "../../views/explore/viewProvider.ts"; + +/** + * The region (saved) view focus selector resolves its definition live + * from the current view list, so editing the active view is reflected on + * refresh and removing it yields no focus (clearing the Resources view). + */ +suite("resolveRegionViewFocus", () => { + const profile = "default"; + const region = "us-east-1"; + + function view(resources: SavedView["resources"]): SavedView { + return { name: "My View", resources, scope: { region } }; + } + + test("resolves the focus from the matching view's pairs", () => { + const focus = resolveRegionViewFocus(profile, region, "My View", [ + view([{ service: "sqs", resourceType: "queue" }]), + ]); + assert.ok(focus); + const services = focus.profiles[0].regions[0].services; + assert.deepStrictEqual( + services.map((s) => s.id), + ["sqs"], + ); + }); + + test("reflects an edit to the view's pairs (live resolution)", () => { + /* Same name, different pairs — simulating an edit; the resolved focus + * must reflect the new pairs, not a stale snapshot. */ + const focus = resolveRegionViewFocus(profile, region, "My View", [ + view([ + { service: "sqs", resourceType: "queue" }, + { service: "sns", resourceType: "topic" }, + ]), + ]); + assert.ok(focus); + const serviceIds = focus.profiles[0].regions[0].services + .map((s) => s.id) + .sort(); + assert.deepStrictEqual(serviceIds, ["sns", "sqs"]); + }); + + test("yields undefined when the named view is absent (removed/renamed)", () => { + const focus = resolveRegionViewFocus( + profile, + region, + "My View", + [view([{ service: "sqs", resourceType: "queue" }])].filter( + (v) => v.name !== "My View", + ), + ); + assert.strictEqual(focus, undefined); + }); +}); diff --git a/src/test/resources/treeItems.test.ts b/src/test/resources/treeItems.test.ts new file mode 100644 index 0000000..b338219 --- /dev/null +++ b/src/test/resources/treeItems.test.ts @@ -0,0 +1,66 @@ +import assert from "node:assert"; + +import { ThemeIcon } from "vscode"; + +import type { + ProfileFocus, + RegionFocus, + ResourceTypeFocus, + ServiceFocus, +} from "../../models/focus.ts"; +import type { ServiceProvider } from "../../platforms/aws/services/serviceProvider.ts"; +import { + ResourceProfileTreeItem, + ResourceRegionTreeItem, + ResourceServiceTypeTreeItem, +} from "../../views/resources/treeItems.ts"; + +/** + * The combined service-and-resource-type row carries a target-aware icon: the + * LocalStack mark for LocalStack-targeted profiles and a generic `cloud` + * codicon for AWS-targeted profiles. No AWS-derived service icon is used. + */ +suite("resource browser tree item icons", () => { + const profile: ProfileFocus = { id: "p", regions: [] }; + const region: RegionFocus = { id: "us-east-1", services: [] }; + const resourceType: ResourceTypeFocus = { id: "queues", arns: [] }; + const service: ServiceFocus = { id: "sqs", resourcetypes: [resourceType] }; + + /* The row only calls getName + getResourceTypeNames on its provider. */ + const provider = { + getName: () => "SQS", + getResourceTypeNames: () => ["Queue", "Queues"], + } as unknown as ServiceProvider; + + function rowForTarget(isLocalStack: boolean): ResourceServiceTypeTreeItem { + const profileItem = new ResourceProfileTreeItem( + profile, + "000000000000", + "", + isLocalStack, + ); + const regionItem = new ResourceRegionTreeItem( + profileItem, + region, + "US East", + ); + return new ResourceServiceTypeTreeItem( + regionItem, + service, + provider, + resourceType, + ); + } + + test("LocalStack-targeted row shows the LocalStack mark", () => { + const icon = rowForTarget(true).iconPath; + assert.ok(icon instanceof ThemeIcon); + assert.strictEqual(icon.id, "localstack-logo"); + }); + + test("AWS-targeted row shows a generic cloud icon", () => { + const icon = rowForTarget(false).iconPath; + assert.ok(icon instanceof ThemeIcon); + assert.strictEqual(icon.id, "cloud"); + }); +}); diff --git a/src/test/services/batch1Providers.test.ts b/src/test/services/batch1Providers.test.ts new file mode 100644 index 0000000..d7bca09 --- /dev/null +++ b/src/test/services/batch1Providers.test.ts @@ -0,0 +1,337 @@ +import assert from "node:assert"; + +import ARN from "../../platforms/aws/models/arnModel.ts"; +import { DeclarativeServiceProvider } from "../../platforms/aws/services/declarative/engine.ts"; +import type { ServiceDefinition } from "../../platforms/aws/services/declarative/types.ts"; +import { apiGatewayDefinition } from "../../platforms/aws/services/definitions/apigateway.ts"; +import { eventsDefinition } from "../../platforms/aws/services/definitions/events.ts"; +import { kinesisDefinition } from "../../platforms/aws/services/definitions/kinesis.ts"; +import { kmsDefinition } from "../../platforms/aws/services/definitions/kms.ts"; +import { logsDefinition } from "../../platforms/aws/services/definitions/logs.ts"; +import { s3Definition } from "../../platforms/aws/services/definitions/s3.ts"; +import { ssmDefinition } from "../../platforms/aws/services/definitions/ssm.ts"; +import { FieldType } from "../../platforms/aws/services/serviceProvider.ts"; + +/** + * End-to-end tests for representative Batch 1 declarative providers, driven + * through the real engine with a stubbed SDK client. They cover each distinct + * pattern: self-detail vs. explicit describe, nested response paths, multi-type + * ARN resolution (by token and by `matchArn` predicate), parent iteration, and + * region-synthesized ARNs for ARN-less list responses. No real AWS calls. + */ + +/* A fake SDK client whose `send` dispatches on the command class name. */ +type Handlers = Record) => unknown>; +function fakeClient(handlers: Handlers): { + send: (command: unknown) => unknown; +} { + return { + send: (command: unknown) => { + const name = (command as { constructor: { name: string } }).constructor + .name; + const handler = handlers[name]; + if (!handler) { + throw new Error(`Unexpected SDK command: ${name}`); + } + const input = + (command as { input?: Record }).input ?? {}; + return Promise.resolve(handler(input)); + }, + }; +} + +/** Build a provider from a real definition but with a stubbed client. */ +function providerWith( + definition: ServiceDefinition, + client: unknown, +) { + return new DeclarativeServiceProvider({ + ...definition, + client: () => client as TClient, + }); +} + +suite("Batch 1: S3 (self-detail, single type)", () => { + const client = fakeClient({ + ListBucketsCommand: () => ({ + Buckets: [{ Name: "my-bucket", CreationDate: new Date(0) }], + }), + }); + + test("lists buckets as region-less ARNs", async () => { + const arns = await providerWith(s3Definition, client).getResourceArns( + "default", + "us-east-1", + "bucket", + ); + assert.deepStrictEqual(arns, ["arn:aws:s3:::my-bucket"]); + }); + + test("describes a bucket from the list item", async () => { + const fields = await providerWith(s3Definition, client).describeResource( + "default", + new ARN("arn:aws:s3:::my-bucket"), + ); + assert.deepStrictEqual(fields, [ + { field: "Name", value: "my-bucket", type: FieldType.NAME }, + { + field: "Creation Date", + value: "1970-01-01T00:00:00.000Z", + type: FieldType.DATE, + }, + ]); + }); +}); + +suite("Batch 1: KMS (multi-type, nested describe + self)", () => { + const keyArn = "arn:aws:kms:us-east-1:000000000000:key/abc-123"; + const aliasArn = "arn:aws:kms:us-east-1:000000000000:alias/my-alias"; + const client = fakeClient({ + ListKeysCommand: () => ({ Keys: [{ KeyId: "abc-123", KeyArn: keyArn }] }), + DescribeKeyCommand: (input) => ({ + KeyMetadata: { + KeyId: "abc-123", + Arn: input.KeyId, + KeyState: "Enabled", + KeyUsage: "ENCRYPT_DECRYPT", + Enabled: true, + }, + }), + ListAliasesCommand: () => ({ + Aliases: [ + { + AliasName: "alias/my-alias", + AliasArn: aliasArn, + TargetKeyId: "abc-123", + }, + ], + }), + }); + + test("describes a key via DescribeKey (nested KeyMetadata path)", async () => { + const fields = await providerWith(kmsDefinition, client).describeResource( + "default", + new ARN(keyArn), + ); + assert.deepStrictEqual(fields[0], { + field: "Key ID", + value: "abc-123", + type: FieldType.NAME, + }); + assert.ok(fields.some((f) => f.field === "State" && f.value === "Enabled")); + }); + + test("resolves an alias ARN to the alias type and describes it via self", async () => { + const fields = await providerWith(kmsDefinition, client).describeResource( + "default", + new ARN(aliasArn), + ); + assert.deepStrictEqual(fields[0], { + field: "Alias Name", + value: "alias/my-alias", + type: FieldType.NAME, + }); + }); +}); + +suite("Batch 1: Kinesis (matchArn predicate, parent iteration)", () => { + const streamArn = "arn:aws:kinesis:us-east-1:000000000000:stream/orders"; + const consumerArn = `${streamArn}/consumer/reader:1700000000`; + const client = fakeClient({ + ListStreamsCommand: () => ({ + StreamSummaries: [{ StreamName: "orders", StreamARN: streamArn }], + }), + ListStreamConsumersCommand: () => ({ + Consumers: [{ ConsumerName: "reader", ConsumerARN: consumerArn }], + }), + DescribeStreamConsumerCommand: () => ({ + ConsumerDescription: { + ConsumerName: "reader", + ConsumerARN: consumerArn, + ConsumerStatus: "ACTIVE", + }, + }), + }); + + test("lists consumers across streams", async () => { + const arns = await providerWith(kinesisDefinition, client).getResourceArns( + "default", + "us-east-1", + "streamconsumer", + ); + assert.deepStrictEqual(arns, [consumerArn]); + }); + + test("a consumer ARN resolves to the consumer type via matchArn", async () => { + const fields = await providerWith( + kinesisDefinition, + client, + ).describeResource("default", new ARN(consumerArn)); + assert.ok(fields.some((f) => f.field === "Status" && f.value === "ACTIVE")); + }); +}); + +suite("Batch 1: SSM document (synthesized ARN + nested describe)", () => { + const client = fakeClient({ + ListDocumentsCommand: () => ({ + DocumentIdentifiers: [{ Name: "My-Doc", DocumentType: "Command" }], + }), + DescribeDocumentCommand: (input) => ({ + Document: { + Name: input.Name, + DocumentType: "Command", + Status: "Active", + Owner: "self", + }, + }), + }); + + test("synthesizes a document ARN from the region", async () => { + const arns = await providerWith(ssmDefinition, client).getResourceArns( + "default", + "eu-west-1", + "document", + ); + assert.deepStrictEqual(arns, ["arn:aws:ssm:eu-west-1::document/My-Doc"]); + }); + + test("resolves the document type and describes it", async () => { + const fields = await providerWith(ssmDefinition, client).describeResource( + "default", + new ARN("arn:aws:ssm:eu-west-1::document/My-Doc"), + ); + assert.deepStrictEqual(fields[0], { + field: "Name", + value: "My-Doc", + type: FieldType.NAME, + }); + assert.ok(fields.some((f) => f.field === "Status" && f.value === "Active")); + }); +}); + +suite("Batch 1: EventBridge archive (region-synthesized ARN)", () => { + const client = fakeClient({ + ListArchivesCommand: () => ({ + Archives: [{ ArchiveName: "audit", State: "ENABLED", EventCount: 5 }], + }), + DescribeArchiveCommand: (input) => ({ + ArchiveName: input.ArchiveName, + State: "ENABLED", + EventCount: 5, + }), + }); + + test("synthesizes an account-less archive ARN", async () => { + const arns = await providerWith(eventsDefinition, client).getResourceArns( + "default", + "us-east-1", + "archive", + ); + assert.deepStrictEqual(arns, ["arn:aws:events:us-east-1::archive/audit"]); + }); + + test("resolves and describes the archive", async () => { + const fields = await providerWith( + eventsDefinition, + client, + ).describeResource( + "default", + new ARN("arn:aws:events:us-east-1::archive/audit"), + ); + assert.deepStrictEqual(fields[0], { + field: "Name", + value: "audit", + type: FieldType.NAME, + }); + }); +}); + +suite("Batch 1: API Gateway stage (augmented item, path-style ARN)", () => { + const client = fakeClient({ + GetRestApisCommand: () => ({ items: [{ id: "api1", name: "My API" }] }), + GetStagesCommand: () => ({ + item: [{ stageName: "prod", deploymentId: "dep1" }], + }), + }); + + test("synthesizes a path-style stage ARN including the api id", async () => { + const arns = await providerWith( + apiGatewayDefinition, + client, + ).getResourceArns("default", "us-east-1", "stage"); + assert.deepStrictEqual(arns, [ + "arn:aws:apigateway:us-east-1::/restapis/api1/stages/prod", + ]); + }); + + test("resolves the stage type via the /stages/ predicate and self-describes", async () => { + const fields = await providerWith( + apiGatewayDefinition, + client, + ).describeResource( + "default", + new ARN("arn:aws:apigateway:us-east-1::/restapis/api1/stages/prod"), + ); + assert.deepStrictEqual(fields[0], { + field: "Stage Name", + value: "prod", + type: FieldType.NAME, + }); + assert.ok( + fields.some((f) => f.field === "REST API ID" && f.value === "api1"), + ); + }); +}); + +suite( + "Batch 1: CloudWatch Logs metric filter (synth ARN with slashes, self)", + () => { + const client = fakeClient({ + DescribeMetricFiltersCommand: () => ({ + metricFilters: [ + { + filterName: "Errors", + logGroupName: "/aws/lambda/fn", + filterPattern: "ERROR", + creationTime: 0, + }, + ], + }), + }); + + test("synthesizes a metric-filter ARN embedding the log group path", async () => { + const arns = await providerWith(logsDefinition, client).getResourceArns( + "default", + "us-east-1", + "metricfilter", + ); + assert.deepStrictEqual(arns, [ + "arn:aws:logs:us-east-1::metric-filter:/aws/lambda/fn:Errors", + ]); + }); + + test("resolves the metric-filter type and renders detail from the list item", async () => { + const fields = await providerWith( + logsDefinition, + client, + ).describeResource( + "default", + new ARN("arn:aws:logs:us-east-1::metric-filter:/aws/lambda/fn:Errors"), + ); + assert.deepStrictEqual(fields, [ + { field: "Name", value: "Errors", type: FieldType.NAME }, + { + field: "Log Group", + value: "/aws/lambda/fn", + type: FieldType.LOG_GROUP, + }, + { field: "Pattern", value: "ERROR", type: FieldType.SHORT_TEXT }, + { + field: "Creation Time", + value: "1970-01-01T00:00:00.000Z", + type: FieldType.DATE, + }, + ]); + }); + }, +); diff --git a/src/test/services/batch2Providers.test.ts b/src/test/services/batch2Providers.test.ts new file mode 100644 index 0000000..6837e4e --- /dev/null +++ b/src/test/services/batch2Providers.test.ts @@ -0,0 +1,460 @@ +import assert from "node:assert"; + +import type { StackResourceSummary } from "@aws-sdk/client-cloudformation"; + +import ARN from "../../platforms/aws/models/arnModel.ts"; +import { DeclarativeServiceProvider } from "../../platforms/aws/services/declarative/engine.ts"; +import type { ServiceDefinition } from "../../platforms/aws/services/declarative/types.ts"; +import { cloudFormationDefinition } from "../../platforms/aws/services/definitions/cloudformation.ts"; +import { dynamoDbDefinition } from "../../platforms/aws/services/definitions/dynamodb.ts"; +import { iamDefinition } from "../../platforms/aws/services/definitions/iam.ts"; +import { lambdaDefinition } from "../../platforms/aws/services/definitions/lambda.ts"; +import { snsDefinition } from "../../platforms/aws/services/definitions/sns.ts"; +import { sqsDefinition } from "../../platforms/aws/services/definitions/sqs.ts"; +import { statesDefinition } from "../../platforms/aws/services/definitions/states.ts"; +import { FieldType } from "../../platforms/aws/services/serviceProvider.ts"; + +/** + * End-to-end tests for the Batch 2 declarative providers — the services + * migrated from hand-written `ServiceProvider` subclasses. They are driven + * through the real engine with a stubbed SDK client (dispatching on command + * class name), covering the patterns unique to this batch: describe-time value + * flattening (joins, boolean→string, URL-decode, epoch coercion), URL↔ARN + * round-trips, ARN-prefix derivation, status filtering, and the variable-length + * `list` detail spec. CloudFormation mapping is asserted directly on the + * provider. No real AWS calls. + */ + +type Handlers = Record) => unknown>; +function fakeClient(handlers: Handlers): { + send: (command: unknown) => unknown; +} { + return { + send: (command: unknown) => { + const name = (command as { constructor: { name: string } }).constructor + .name; + const handler = handlers[name]; + if (!handler) { + throw new Error(`Unexpected SDK command: ${name}`); + } + const input = + (command as { input?: Record }).input ?? {}; + return Promise.resolve(handler(input)); + }, + }; +} + +function providerWith( + definition: ServiceDefinition, + client: unknown, +) { + return new DeclarativeServiceProvider({ + ...definition, + client: () => client as TClient, + }); +} + +function summary(partial: Partial): StackResourceSummary { + return partial as StackResourceSummary; +} + +suite("Batch 2: IAM (path-stripped GetRole, decoded policy)", () => { + const roleArn = "arn:aws:iam::000000000000:role/my-team/my-role"; + let seenRoleName: string | undefined; + const client = fakeClient({ + ListRolesCommand: () => ({ + Roles: [{ Arn: roleArn, RoleName: "my-role" }], + }), + GetRoleCommand: (input) => { + seenRoleName = input.RoleName as string; + return { + Role: { + RoleName: "my-role", + RoleId: "AROA123", + AssumeRolePolicyDocument: encodeURIComponent('{"Version":"2012"}'), + }, + }; + }, + }); + + test("lists role ARNs", async () => { + const arns = await providerWith(iamDefinition, client).getResourceArns( + "default", + "us-east-1", + "role", + ); + assert.deepStrictEqual(arns, [roleArn]); + }); + + test("strips the path before GetRole and decodes the policy document", async () => { + const fields = await providerWith(iamDefinition, client).describeResource( + "default", + new ARN(roleArn), + ); + assert.strictEqual(seenRoleName, "my-role"); + const policy = fields.find((f) => f.field === "Assume Role Policy"); + assert.deepStrictEqual(policy, { + field: "Assume Role Policy", + value: '{"Version":"2012"}', + type: FieldType.JSON, + }); + }); + + test("maps a CloudFormation role to role/", () => { + assert.deepStrictEqual( + providerWith( + iamDefinition, + client, + ).getArnResourceNameForCloudFormationResource( + summary({ ResourceType: "AWS::IAM::Role", PhysicalResourceId: "r1" }), + ), + { resourceType: "role", resourceName: "role/r1" }, + ); + }); +}); + +suite("Batch 2: Lambda (multi-type, joined arrays, typed CFN names)", () => { + const fnArn = "arn:aws:lambda:us-east-1:000000000000:function:my-fn"; + const esmArn = + "arn:aws:lambda:us-east-1:000000000000:event-source-mapping:uuid-1"; + const client = fakeClient({ + ListFunctionsCommand: () => ({ Functions: [{ FunctionArn: fnArn }] }), + GetFunctionCommand: () => ({ + Configuration: { + FunctionName: "my-fn", + Architectures: ["arm64", "x86_64"], + }, + }), + ListEventSourceMappingsCommand: () => ({ + EventSourceMappings: [{ EventSourceMappingArn: esmArn }], + }), + GetEventSourceMappingCommand: () => ({ UUID: "uuid-1", State: "Enabled" }), + }); + + test("lists function ARNs", async () => { + const arns = await providerWith(lambdaDefinition, client).getResourceArns( + "default", + "us-east-1", + "function", + ); + assert.deepStrictEqual(arns, [fnArn]); + }); + + test("joins the architectures array for display", async () => { + const fields = await providerWith( + lambdaDefinition, + client, + ).describeResource("default", new ARN(fnArn)); + assert.ok( + fields.some( + (f) => f.field === "Architectures" && f.value === "arm64, x86_64", + ), + ); + }); + + test("resolves an event-source-mapping ARN to its own type", async () => { + const fields = await providerWith( + lambdaDefinition, + client, + ).describeResource("default", new ARN(esmArn)); + assert.deepStrictEqual(fields[0], { + field: "UUID", + value: "uuid-1", + type: FieldType.NAME, + }); + }); + + test("encodes the type in the CloudFormation resource name", () => { + const provider = providerWith(lambdaDefinition, client); + assert.deepStrictEqual( + provider.getArnResourceNameForCloudFormationResource( + summary({ + ResourceType: "AWS::Lambda::Function", + PhysicalResourceId: "my-fn", + }), + ), + { resourceType: "function", resourceName: "function:my-fn" }, + ); + assert.deepStrictEqual( + provider.getArnResourceNameForCloudFormationResource( + summary({ + ResourceType: "AWS::Lambda::EventSourceMapping", + PhysicalResourceId: "uuid-1", + }), + ), + { + resourceType: "event-source-mapping", + resourceName: "event-source-mapping:uuid-1", + }, + ); + }); +}); + +suite("Batch 2: SNS (name from ARN + attributes)", () => { + const topicArn = "arn:aws:sns:us-east-1:000000000000:my-topic"; + const client = fakeClient({ + ListTopicsCommand: () => ({ Topics: [{ TopicArn: topicArn }] }), + GetTopicAttributesCommand: () => ({ + Attributes: { DisplayName: "My Topic", SubscriptionsConfirmed: "3" }, + }), + }); + + test("takes the name from the ARN and the rest from attributes", async () => { + const fields = await providerWith(snsDefinition, client).describeResource( + "default", + new ARN(topicArn), + ); + assert.deepStrictEqual(fields[0], { + field: "Name", + value: "my-topic", + type: FieldType.NAME, + }); + assert.ok( + fields.some((f) => f.field === "Display Name" && f.value === "My Topic"), + ); + }); +}); + +suite("Batch 2: SQS (URL↔ARN round-trip, epoch coercion)", () => { + const queueUrl = "https://sqs.us-east-1.amazonaws.com/000000000000/my-queue"; + const queueArn = "arn:aws:sqs:us-east-1:000000000000:my-queue"; + const client = fakeClient({ + ListQueuesCommand: () => ({ QueueUrls: [queueUrl] }), + GetQueueUrlCommand: () => ({ QueueUrl: queueUrl }), + GetQueueAttributesCommand: (input) => { + const names = input.AttributeNames as string[]; + if (names.includes("QueueArn")) { + return { Attributes: { QueueArn: queueArn } }; + } + return { + Attributes: { VisibilityTimeout: "30", CreatedTimestamp: "0" }, + }; + }, + }); + + test("resolves listed queue URLs to ARNs", async () => { + const arns = await providerWith(sqsDefinition, client).getResourceArns( + "default", + "us-east-1", + "queue", + ); + assert.deepStrictEqual(arns, [queueArn]); + }); + + test("renders the epoch-second timestamp as an ISO date", async () => { + const fields = await providerWith(sqsDefinition, client).describeResource( + "default", + new ARN(queueArn), + ); + assert.ok( + fields.some( + (f) => + f.field === "Created Timestamp" && + f.value === "1970-01-01T00:00:00.000Z", + ), + ); + }); + + test("maps a CloudFormation queue URL to its name", () => { + assert.deepStrictEqual( + providerWith( + sqsDefinition, + client, + ).getArnResourceNameForCloudFormationResource( + summary({ + ResourceType: "AWS::SQS::Queue", + PhysicalResourceId: queueUrl, + }), + ), + { resourceType: "queue", resourceName: "my-queue" }, + ); + }); +}); + +suite( + "Batch 2: Step Functions (self activity, flattened state machine)", + () => { + const activityArn = "arn:aws:states:us-east-1:000000000000:activity:act"; + const machineArn = "arn:aws:states:us-east-1:000000000000:stateMachine:sm"; + const logGroupArn = "arn:aws:logs:us-east-1:000000000000:log-group:/sm"; + const client = fakeClient({ + ListActivitiesCommand: () => ({ + activities: [{ activityArn, name: "act", creationDate: new Date(0) }], + }), + ListStateMachinesCommand: () => ({ + stateMachines: [{ stateMachineArn: machineArn, name: "sm" }], + }), + DescribeStateMachineCommand: () => ({ + name: "sm", + loggingConfiguration: { + includeExecutionData: true, + level: "ALL", + destinations: [{ cloudWatchLogsLogGroup: { logGroupArn } }], + }, + tracingConfiguration: { enabled: true }, + }), + }); + + test("describes an activity from its list item (self)", async () => { + const fields = await providerWith( + statesDefinition, + client, + ).describeResource("default", new ARN(activityArn)); + assert.deepStrictEqual(fields, [ + { field: "Name", value: "act", type: FieldType.NAME }, + { + field: "Creation Date", + value: "1970-01-01T00:00:00.000Z", + type: FieldType.DATE, + }, + ]); + }); + + test("flattens logging/tracing config into display strings", async () => { + const fields = await providerWith( + statesDefinition, + client, + ).describeResource("default", new ARN(machineArn)); + assert.ok( + fields.some((f) => f.field === "Log Group" && f.value === logGroupArn), + ); + assert.ok( + fields.some( + (f) => f.field === "Log Execution Data" && f.value === "Yes", + ), + ); + assert.ok( + fields.some((f) => f.field === "Tracing" && f.value === "Enabled"), + ); + }); + + test("does not support CloudFormation mapping", () => { + assert.throws(() => + providerWith( + statesDefinition, + client, + ).getArnResourceNameForCloudFormationResource( + summary({ + ResourceType: "AWS::StepFunctions::StateMachine", + PhysicalResourceId: "sm", + }), + ), + ); + }); + }, +); + +suite("Batch 2: DynamoDB (ARN-prefix derivation, list detail spec)", () => { + const tableArn = "arn:aws:dynamodb:us-east-1:000000000000:table/t1"; + const client = fakeClient({ + ListTablesCommand: () => ({ TableNames: ["t1", "t2"] }), + DescribeTableCommand: (input) => ({ + Table: { + TableName: input.TableName, + TableArn: `arn:aws:dynamodb:us-east-1:000000000000:table/${input.TableName as string}`, + AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], + KeySchema: [{ AttributeName: "id", KeyType: "HASH" }], + }, + }), + }); + + test("derives every table ARN from the first table's prefix", async () => { + const arns = await providerWith(dynamoDbDefinition, client).getResourceArns( + "default", + "us-east-1", + "table", + ); + assert.deepStrictEqual(arns, [ + "arn:aws:dynamodb:us-east-1:000000000000:table/t1", + "arn:aws:dynamodb:us-east-1:000000000000:table/t2", + ]); + }); + + test("expands attribute definitions and key schema via the list spec", async () => { + const fields = await providerWith( + dynamoDbDefinition, + client, + ).describeResource("default", new ARN(tableArn)); + assert.deepStrictEqual( + fields.filter((f) => + ["Attributes", "Key Schema", " id"].includes(f.field), + ), + [ + { field: "Attributes", value: "", type: FieldType.NAME }, + { field: " id", value: "S", type: FieldType.NAME }, + { field: "Key Schema", value: "", type: FieldType.NAME }, + { field: " id", value: "HASH", type: FieldType.NAME }, + ], + ); + }); +}); + +suite("Batch 2: CloudFormation (status filter, sort, list detail spec)", () => { + const stackArn = + "arn:aws:cloudformation:us-east-1:000000000000:stack/my-stack/abc"; + let seenStatusFilter: unknown; + const client = fakeClient({ + ListStacksCommand: (input) => { + seenStatusFilter = input.StackStatusFilter; + return { + StackSummaries: [ + { StackId: "arn:...:stack/b" }, + { StackId: "arn:...:stack/a" }, + ], + }; + }, + DescribeStacksCommand: () => ({ + Stacks: [ + { + StackName: "my-stack", + EnableTerminationProtection: true, + DisableRollback: false, + Capabilities: ["CAPABILITY_IAM"], + Parameters: [{ ParameterKey: "Env", ParameterValue: "prod" }], + Outputs: [{ OutputKey: "Url", OutputValue: "https://x" }], + }, + ], + }), + }); + + test("filters to created statuses and returns ARNs sorted by id", async () => { + const arns = await providerWith( + cloudFormationDefinition, + client, + ).getResourceArns("default", "us-east-1", "stack"); + assert.deepStrictEqual(arns, ["arn:...:stack/a", "arn:...:stack/b"]); + assert.ok(Array.isArray(seenStatusFilter)); + assert.ok((seenStatusFilter as string[]).includes("CREATE_COMPLETE")); + }); + + test("flattens flags and expands parameters/outputs", async () => { + const fields = await providerWith( + cloudFormationDefinition, + client, + ).describeResource("default", new ARN(stackArn)); + assert.ok( + fields.some( + (f) => f.field === "Termination Protection" && f.value === "Enabled", + ), + ); + assert.ok( + fields.some((f) => f.field === "Rollback" && f.value === "Enabled"), + ); + assert.ok( + fields.some( + (f) => f.field === "Capabilities" && f.value === "CAPABILITY_IAM", + ), + ); + assert.deepStrictEqual( + fields.filter((f) => + ["Parameters", " Env", "Outputs", " Url"].includes(f.field), + ), + [ + { field: "Parameters", value: "", type: FieldType.NAME }, + { field: " Env", value: "prod", type: FieldType.NAME }, + { field: "Outputs", value: "", type: FieldType.NAME }, + { field: " Url", value: "https://x", type: FieldType.NAME }, + ], + ); + }); +}); diff --git a/src/test/services/cfnRoundTrip.test.ts b/src/test/services/cfnRoundTrip.test.ts new file mode 100644 index 0000000..a67bb2b --- /dev/null +++ b/src/test/services/cfnRoundTrip.test.ts @@ -0,0 +1,186 @@ +import assert from "node:assert"; + +import type { StackResourceSummary } from "@aws-sdk/client-cloudformation"; + +import ARN from "../../platforms/aws/models/arnModel.ts"; +import { DeclarativeServiceProvider } from "../../platforms/aws/services/declarative/engine.ts"; +import type { ServiceDefinition } from "../../platforms/aws/services/declarative/types.ts"; +import { apiGatewayDefinition } from "../../platforms/aws/services/definitions/apigateway.ts"; +import { cognitoIdpDefinition } from "../../platforms/aws/services/definitions/cognito-idp.ts"; + +/** + * Round-trip tests for CloudFormation-origin resources: synthesize the ARN + * exactly as `CfnStackModel` does (`getArnResourceNameForCloudFormationResource` + * → `arn:aws::::`), then `describeResource` + * that ARN and assert it resolves to the right type and fetches by the right id. + * + * This is the gap that let API Gateway and Cognito resources throw when opened + * from a stack view: the live `id` and the CloudFormation ARN are built by two + * different code paths, so a type whose `cfnResourceName` doesn't re-encode the + * discriminating token is unresolvable. The dropped types assert the honest + * fallback — they have no `cfn` mapping (CloudFormation can't supply the parent + * id they need), so the stack model skips them rather than showing a broken row. + */ + +type Handlers = Record) => unknown>; +function fakeClient(handlers: Handlers): { + send: (command: unknown) => unknown; +} { + return { + send: (command: unknown) => { + const name = (command as { constructor: { name: string } }).constructor + .name; + const handler = handlers[name]; + if (!handler) { + throw new Error(`Unexpected SDK command: ${name}`); + } + const input = + (command as { input?: Record }).input ?? {}; + return Promise.resolve(handler(input)); + }, + }; +} + +function providerWith( + definition: ServiceDefinition, + client: unknown, +) { + return new DeclarativeServiceProvider({ + ...definition, + client: () => client as TClient, + }); +} + +function summary(partial: Partial): StackResourceSummary { + return partial as StackResourceSummary; +} + +/** Build the ARN a stack resource would get, mirroring `CfnStackModel`. */ +function cfnArn( + provider: DeclarativeServiceProvider, + stackResource: StackResourceSummary, +): { resourceType: string; arn: ARN } { + const { resourceType, resourceName } = + provider.getArnResourceNameForCloudFormationResource(stackResource); + const arn = `arn:aws:${provider.getId()}:us-east-1:000000000000:${resourceName}`; + return { resourceType, arn: new ARN(arn) }; +} + +suite("CloudFormation round-trip: API Gateway", () => { + let seen: Record = {}; + const client = fakeClient({ + GetRestApiCommand: (input) => { + seen.restApiId = input.restApiId as string; + return { id: input.restApiId, name: "My API" }; + }, + GetApiKeyCommand: (input) => { + seen.apiKey = input.apiKey as string; + return { id: input.apiKey, name: "My Key", enabled: true }; + }, + GetUsagePlanCommand: (input) => { + seen.usagePlanId = input.usagePlanId as string; + return { id: input.usagePlanId, name: "My Plan" }; + }, + }); + const provider = providerWith(apiGatewayDefinition, client); + + test("REST API resolves and describes by id", async () => { + seen = {}; + const { resourceType, arn } = cfnArn( + provider, + summary({ + ResourceType: "AWS::ApiGateway::RestApi", + PhysicalResourceId: "abc123", + }), + ); + assert.strictEqual(resourceType, "restapi"); + const fields = await provider.describeResource("default", arn); + assert.strictEqual(seen.restApiId, "abc123"); + assert.ok(fields.some((f) => f.field === "Name" && f.value === "My API")); + }); + + test("API Key resolves and describes by id", async () => { + seen = {}; + const { resourceType, arn } = cfnArn( + provider, + summary({ + ResourceType: "AWS::ApiGateway::ApiKey", + PhysicalResourceId: "key123", + }), + ); + assert.strictEqual(resourceType, "apikey"); + await provider.describeResource("default", arn); + assert.strictEqual(seen.apiKey, "key123"); + }); + + test("Usage Plan resolves and describes by id", async () => { + seen = {}; + const { resourceType, arn } = cfnArn( + provider, + summary({ + ResourceType: "AWS::ApiGateway::UsagePlan", + PhysicalResourceId: "plan123", + }), + ); + assert.strictEqual(resourceType, "usageplan"); + await provider.describeResource("default", arn); + assert.strictEqual(seen.usagePlanId, "plan123"); + }); + + test("Stage and Authorizer have no CloudFormation mapping", () => { + for (const ResourceType of [ + "AWS::ApiGateway::Stage", + "AWS::ApiGateway::Authorizer", + ]) { + assert.throws( + () => + provider.getArnResourceNameForCloudFormationResource( + summary({ ResourceType, PhysicalResourceId: "x" }), + ), + /Unsupported resource type/, + `expected ${ResourceType} to be unmapped`, + ); + } + }); +}); + +suite("CloudFormation round-trip: Cognito", () => { + let seenUserPoolId: string | undefined; + const client = fakeClient({ + DescribeUserPoolCommand: (input) => { + seenUserPoolId = input.UserPoolId as string; + return { UserPool: { Name: "my-pool", Id: input.UserPoolId } }; + }, + }); + const provider = providerWith(cognitoIdpDefinition, client); + + test("User Pool resolves and describes by id", async () => { + const { resourceType, arn } = cfnArn( + provider, + summary({ + ResourceType: "AWS::Cognito::UserPool", + PhysicalResourceId: "us-east-1_AbC123", + }), + ); + assert.strictEqual(resourceType, "userpool"); + const fields = await provider.describeResource("default", arn); + assert.strictEqual(seenUserPoolId, "us-east-1_AbC123"); + assert.ok(fields.some((f) => f.field === "Name" && f.value === "my-pool")); + }); + + test("User Pool Client and Group have no CloudFormation mapping", () => { + for (const ResourceType of [ + "AWS::Cognito::UserPoolClient", + "AWS::Cognito::UserPoolGroup", + ]) { + assert.throws( + () => + provider.getArnResourceNameForCloudFormationResource( + summary({ ResourceType, PhysicalResourceId: "x" }), + ), + /Unsupported resource type/, + `expected ${ResourceType} to be unmapped`, + ); + } + }); +}); diff --git a/src/test/services/declarativeEngine.test.ts b/src/test/services/declarativeEngine.test.ts new file mode 100644 index 0000000..6a76c57 --- /dev/null +++ b/src/test/services/declarativeEngine.test.ts @@ -0,0 +1,236 @@ +import assert from "node:assert"; + +import type { StackResourceSummary } from "@aws-sdk/client-cloudformation"; + +import ARN from "../../platforms/aws/models/arnModel.ts"; +import { + DeclarativeServiceProvider, + formatValue, + getByPath, + renderDetailField, +} from "../../platforms/aws/services/declarative/engine.ts"; +import { defineService } from "../../platforms/aws/services/declarative/types.ts"; +import { FieldType } from "../../platforms/aws/services/serviceProvider.ts"; + +/** + * The declarative engine adapts a `ServiceDefinition` (data + per-type closures) + * to the `ServiceProvider` interface. These tests drive it with a fake service + * whose "SDK client" returns fixed data, exercising resource-type listing, + * identifier mapping, path-based detail rendering (both via an explicit + * `describe` call and via the "self" list-item fallback), multi-type ARN + * resolution, and CloudFormation mapping — without any real AWS calls. + */ + +type Widget = { Arn: string; Name: string; State: string; Created: Date }; +type Gadget = { Arn: string }; + +/* A stand-in SDK client; the definition's closures read from fixed data. */ +type FakeClient = { tag: "fake" }; + +const WIDGET_ARN = "arn:aws:fake:us-east-1:000000000000:widget/w1"; +const GADGET_ARN = "arn:aws:fake:us-east-1:000000000000:gadget/g1"; + +const fakeDefinition = defineService({ + id: "fake", + name: "Fake Service", + client: () => ({ tag: "fake" }), + resourceTypes: { + widget: { + singular: "Widget", + plural: "Widgets", + cfn: "AWS::Fake::Widget", + list: (): Promise => + Promise.resolve([ + { + Arn: WIDGET_ARN, + Name: "w1", + State: "ACTIVE", + Created: new Date(0), + }, + ]), + id: (item: Widget) => item.Arn, + /* no `describe`: detail is read from the matching list item ("self") */ + detail: [ + { label: "Name", path: "Name", type: FieldType.NAME }, + { label: "State", path: "State", type: FieldType.NAME }, + { label: "Created", path: "Created", type: FieldType.DATE }, + ], + }, + gadget: { + singular: "Gadget", + plural: "Gadgets", + cfn: "AWS::Fake::Gadget", + list: (): Promise => Promise.resolve([{ Arn: GADGET_ARN }]), + id: (item: Gadget) => item.Arn, + describe: () => + Promise.resolve({ + Config: { Size: 5 }, + Nested: { Deep: "value" }, + }), + detail: [ + { label: "Size", path: "Config.Size", type: FieldType.NUMBER }, + { label: "Deep", path: "Nested.Deep", type: FieldType.SHORT_TEXT }, + ], + }, + }, +}); + +function makeProvider() { + return new DeclarativeServiceProvider(fakeDefinition); +} + +suite("declarative engine: getByPath", () => { + test("walks dotted paths", () => { + assert.strictEqual(getByPath({ a: { b: { c: 7 } } }, "a.b.c"), 7); + }); + test("walks bracketed array indices", () => { + assert.strictEqual( + getByPath({ Tags: [{ Value: "x" }, { Value: "y" }] }, "Tags[1].Value"), + "y", + ); + }); + test("returns undefined for a missing segment", () => { + assert.strictEqual(getByPath({ a: {} }, "a.b.c"), undefined); + }); +}); + +suite("declarative engine: formatValue", () => { + test("renders epoch-millis numbers as ISO dates", () => { + assert.strictEqual( + formatValue(0, FieldType.DATE), + "1970-01-01T00:00:00.000Z", + ); + }); + test("stringifies objects for JSON fields", () => { + assert.strictEqual( + formatValue({ a: 1 }, FieldType.JSON), + JSON.stringify({ a: 1 }, null, 2), + ); + }); + test("renders null/undefined as empty string", () => { + assert.strictEqual(formatValue(undefined, FieldType.NAME), ""); + assert.strictEqual(formatValue(null, FieldType.NAME), ""); + }); +}); + +suite("declarative engine: renderDetailField", () => { + test("renders a scalar spec as a single row", () => { + assert.deepStrictEqual( + renderDetailField( + { label: "Name", path: "Name", type: FieldType.NAME }, + { Name: "thing" }, + ), + [{ field: "Name", value: "thing", type: FieldType.NAME }], + ); + }); + + test("expands a list spec into a header plus one indented row per item", () => { + const fields = renderDetailField( + { + kind: "list", + label: "Key Schema", + path: "KeySchema", + itemLabel: "AttributeName", + itemValue: "KeyType", + }, + { + KeySchema: [ + { AttributeName: "id", KeyType: "HASH" }, + { AttributeName: "sort", KeyType: "RANGE" }, + ], + }, + ); + assert.deepStrictEqual(fields, [ + { field: "Key Schema", value: "", type: FieldType.NAME }, + { field: " id", value: "HASH", type: FieldType.NAME }, + { field: " sort", value: "RANGE", type: FieldType.NAME }, + ]); + }); + + test("renders just the header when the array is missing or empty", () => { + assert.deepStrictEqual( + renderDetailField( + { + kind: "list", + label: "Outputs", + path: "Outputs", + itemLabel: "OutputKey", + itemValue: "OutputValue", + }, + {}, + ), + [{ field: "Outputs", value: "", type: FieldType.NAME }], + ); + }); +}); + +suite("declarative engine: provider", () => { + test("exposes resource types and their display names", () => { + const provider = makeProvider(); + assert.deepStrictEqual(provider.getResourceTypes().sort(), [ + "gadget", + "widget", + ]); + assert.deepStrictEqual(provider.getResourceTypeNames("widget"), [ + "Widget", + "Widgets", + ]); + }); + + test("getResourceArns maps list items to identifiers", async () => { + const arns = await makeProvider().getResourceArns( + "default", + "us-east-1", + "widget", + ); + assert.deepStrictEqual(arns, [WIDGET_ARN]); + }); + + test("describeResource renders detail from the list item (self)", async () => { + const fields = await makeProvider().describeResource( + "default", + new ARN(WIDGET_ARN), + ); + assert.deepStrictEqual(fields, [ + { field: "Name", value: "w1", type: FieldType.NAME }, + { field: "State", value: "ACTIVE", type: FieldType.NAME }, + { + field: "Created", + value: "1970-01-01T00:00:00.000Z", + type: FieldType.DATE, + }, + ]); + }); + + test("describeResource renders detail from an explicit describe call", async () => { + const fields = await makeProvider().describeResource( + "default", + new ARN(GADGET_ARN), + ); + assert.deepStrictEqual(fields, [ + { field: "Size", value: "5", type: FieldType.NUMBER }, + { field: "Deep", value: "value", type: FieldType.SHORT_TEXT }, + ]); + }); + + test("maps a CloudFormation resource to its type and name", () => { + const summary = { + ResourceType: "AWS::Fake::Widget", + PhysicalResourceId: "w1", + } as StackResourceSummary; + assert.deepStrictEqual( + makeProvider().getArnResourceNameForCloudFormationResource(summary), + { resourceType: "widget", resourceName: "w1" }, + ); + }); + + test("throws for an unmapped CloudFormation type", () => { + const summary = { + ResourceType: "AWS::Other::Thing", + PhysicalResourceId: "x", + } as StackResourceSummary; + assert.throws(() => + makeProvider().getArnResourceNameForCloudFormationResource(summary), + ); + }); +}); diff --git a/src/test/services/providerCompleteness.test.ts b/src/test/services/providerCompleteness.test.ts new file mode 100644 index 0000000..81b5061 --- /dev/null +++ b/src/test/services/providerCompleteness.test.ts @@ -0,0 +1,60 @@ +import assert from "node:assert"; + +import { ProviderFactory } from "../../platforms/aws/services/providerFactory.ts"; +import { getAllServiceIds } from "../../platforms/aws/services/serviceManifest.ts"; + +/** + * Completeness tracking for manifest-backed provider registration. + * + * The definition of "done" for the full-service-coverage effort is: every + * manifest service has a registered provider. Until all batches land this runs + * as a coverage tracker — it never serves a generic fallback, so a manifest + * service with no provider is simply absent. The hard assertions below guard + * against regressions (stray providers, lost coverage); the final + * everything-covered gate is logged until it can be turned on (see the skipped + * test) once the remaining batches ship. + */ +suite("provider completeness", () => { + suiteSetup(() => { + ProviderFactory.initialize(); + }); + + test("every registered provider maps to a real manifest service id", () => { + const manifestIds = new Set(getAllServiceIds()); + const stray = ProviderFactory.getRegisteredServiceIds().filter( + (id) => !manifestIds.has(id), + ); + assert.deepStrictEqual( + stray, + [], + `registered providers with no manifest entry: ${stray.join(", ")}`, + ); + }); + + test("coverage tracker: report manifest services with no provider", () => { + const registered = new Set(ProviderFactory.getRegisteredServiceIds()); + const manifestIds = getAllServiceIds(); + const missing = manifestIds.filter((id) => !registered.has(id)); + + const covered = manifestIds.length - missing.length; + console.log( + `[provider-coverage] ${covered}/${manifestIds.length} manifest services ` + + `have a provider; ${missing.length} remaining.`, + ); + + /* Coverage can only be a subset of the manifest; full coverage is the + * eventual goal, enforced by the skipped test below once batches land. */ + assert.ok(covered > 0, "expected at least one provider registered"); + assert.ok(covered <= manifestIds.length); + }); + + test.skip("DONE GATE: every manifest service has a provider", () => { + const registered = new Set(ProviderFactory.getRegisteredServiceIds()); + const missing = getAllServiceIds().filter((id) => !registered.has(id)); + assert.deepStrictEqual( + missing, + [], + `manifest services with no provider: ${missing.join(", ")}`, + ); + }); +}); diff --git a/src/utils/authenticate.ts b/src/utils/authenticate.ts index 496aff5..205597a 100644 --- a/src/utils/authenticate.ts +++ b/src/utils/authenticate.ts @@ -114,7 +114,7 @@ export async function saveAuthToken( outputChannel.error(error); - window + void window .showErrorMessage( `Failed to save auth token to ${LOCALSTACK_AUTH_FILENAME_READABLE}`, "View Logs", diff --git a/src/utils/container-status.ts b/src/utils/container-status.ts index d09fff3..b49d918 100644 --- a/src/utils/container-status.ts +++ b/src/utils/container-status.ts @@ -18,7 +18,7 @@ export interface ContainerStatusTracker extends Disposable { * Checks the status of a docker container in realtime. */ export async function createContainerStatusTracker( - containerName: string, + containerNames: string[], outputChannel: LogOutputChannel, timeTracker: TimeTracker, ): Promise { @@ -26,7 +26,7 @@ export async function createContainerStatusTracker( const emitter = createEmitter(outputChannel); const disposable = listenToContainerStatus( - containerName, + containerNames, outputChannel, (newStatus) => { if (status !== newStatus) { @@ -37,7 +37,7 @@ export async function createContainerStatusTracker( ); await timeTracker.run("container-status.getContainerStatus", async () => { - await getContainerStatus(containerName).then((newStatus) => { + await getContainerStatus(containerNames).then((newStatus) => { status ??= newStatus; void emitter.emit(status); }); @@ -70,7 +70,7 @@ const DockerEventsSchema = z.object({ }); function listenToContainerStatus( - containerName: string, + containerNames: string[], outputChannel: LogOutputChannel, onStatusChange: (status: ContainerStatus) => void, ): Disposable { @@ -78,6 +78,13 @@ function listenToContainerStatus( let isDisposed = false; let restartTimeout: NodeJS.Timeout | undefined; + /* Docker OR's repeated --filter container=… values together. */ + const containerFilters = containerNames.flatMap((name) => [ + "--filter", + `container=${name}`, + ]); + const watchedNames = new Set(containerNames); + const startListening = () => { if (isDisposed) return; @@ -88,8 +95,7 @@ function listenToContainerStatus( try { dockerEvents = spawn("docker", [ "events", - "--filter", - `container=${containerName}`, + ...containerFilters, "--filter", "event=start", "--filter", @@ -137,7 +143,7 @@ function listenToContainerStatus( return; } - if (parsed.data.Actor.Attributes.name !== containerName) { + if (!watchedNames.has(parsed.data.Actor.Attributes.name)) { return; } @@ -194,6 +200,18 @@ function listenToContainerStatus( } async function getContainerStatus( + containerNames: string[], +): Promise { + const statuses = await Promise.all( + containerNames.map((name) => getSingleContainerStatus(name)), + ); + /* Report the most-alive status across all watched names. */ + if (statuses.includes("running")) return "running"; + if (statuses.includes("stopping")) return "stopping"; + return "stopped"; +} + +async function getSingleContainerStatus( containerName: string, ): Promise { return new Promise((resolve) => { diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..2178213 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,20 @@ +/** + * Indicates that something happened inside the software that is not + * the user's fault. It requires a software fix. + */ +export class InternalError extends Error { + constructor(message: string) { + super(message); + this.name = "InternalError"; + } +} + +/** + * Indicates there was a configuration problem that the user needs to fix. + */ +export class UserConfigurationError extends Error { + constructor(message: string) { + super(message); + this.name = "UserConfigurationError"; + } +} diff --git a/src/utils/install.ts b/src/utils/install.ts index e392ab9..07a8f07 100644 --- a/src/utils/install.ts +++ b/src/utils/install.ts @@ -107,7 +107,9 @@ export async function runInstallProcess( ); if (!installScope) { - window.showErrorMessage("The installation was cancelled by the user"); + void window.showErrorMessage( + "The installation was cancelled by the user", + ); return { cancelled: true }; } @@ -466,12 +468,12 @@ fi profileFile, `\n# Added by LocalStack installer\n${sourceLocalstackInShellProfile}\n`, ); - window.showInformationMessage( + void window.showInformationMessage( `Updated your shell profile (${profileFile}) to include ~/.local/bin in PATH. Restart your terminal to apply changes.`, ); } } else { - window.showInformationMessage( + void window.showInformationMessage( `Could not detect your shell profile. To use localstack CLI from terminal ensure ~/.local/bin is in your PATH.`, ); } @@ -483,7 +485,7 @@ fi process.env.PATH = `${localBinPath}:${currentPath}`; } } catch (err) { - window.showInformationMessage( + void window.showInformationMessage( `Could not update your shell profile. To use localstack CLI from terminal ensure ~/.local/bin is in your PATH.`, ); } @@ -514,7 +516,9 @@ async function installLocalDarwinLinux(temporaryDirname: string) { await ensureLocalBinInPath(); - window.showInformationMessage("LocalStack CLI installed for current user."); + void window.showInformationMessage( + "LocalStack CLI installed for current user.", + ); } async function installLocalWindows(temporaryDirname: string) { @@ -522,7 +526,9 @@ async function installLocalWindows(temporaryDirname: string) { await move(`${temporaryDirname}/localstack`, LOCAL_CLI_INSTALLATION_DIRNAME); await exec(`setx PATH "%PATH%;${LOCAL_CLI_INSTALLATION_DIRNAME}"`); - window.showInformationMessage("LocalStack CLI installed for current user."); + void window.showInformationMessage( + "LocalStack CLI installed for current user.", + ); } async function installGlobalDarwin( @@ -542,7 +548,7 @@ async function installGlobalDarwin( }); if (cancelled) { //TODO check if progress can be used instead of window - window.showErrorMessage("The installation was cancelled by the user"); + void window.showErrorMessage("The installation was cancelled by the user"); return { cancelled: true }; } } @@ -565,12 +571,14 @@ async function installGlobalLinux( }); if (cancelled) { //TODO check if progress can be used instead of window - window.showErrorMessage("The installation was cancelled by the user"); + void window.showErrorMessage( + "The installation was cancelled by the user", + ); return { cancelled: true }; } } catch (error) { const message = error instanceof Error ? error.message : String(error); - window.showErrorMessage(`Installation failed: ${message}`); + void window.showErrorMessage(`Installation failed: ${message}`); throw error; } } @@ -636,7 +644,7 @@ async function installGlobalWindows( cancellationToken, }); - window.showInformationMessage( + void window.showInformationMessage( "LocalStack CLI installed globally for all users.", ); } diff --git a/src/utils/license.ts b/src/utils/license.ts index 631d01e..a41ef5a 100644 --- a/src/utils/license.ts +++ b/src/utils/license.ts @@ -12,7 +12,10 @@ import { execLocalStack } from "./cli.ts"; const cacheDirectory = () => { switch (platform()) { case "win32": - return join(process.env.LOCALAPPDATA!, "cache"); + return join( + process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), + "cache", + ); case "darwin": return join(homedir(), "Library", "Caches"); default: diff --git a/src/utils/localstack-endpoint.ts b/src/utils/localstack-endpoint.ts new file mode 100644 index 0000000..97a03a0 --- /dev/null +++ b/src/utils/localstack-endpoint.ts @@ -0,0 +1,48 @@ +import { readFile } from "node:fs/promises"; + +import { AWS_CONFIG_FILENAME } from "./configure-aws.ts"; +import { parseIni } from "./ini-parser.ts"; + +/** + * The endpoint used when the `localstack` AWS profile has not been configured + * yet. Mirrors the default the configure-aws plugin writes when DNS resolves. + */ +const DEFAULT_ENDPOINT = "http://localhost.localstack.cloud:4566"; + +const LOCALSTACK_PROFILE_SECTION = "profile localstack"; + +/** + * Return the endpoint URL the Toolkit is configured to use for the local + * emulator. This is the single source of truth shared by the LocalStack + * instance label, the metamodel fetch, and the SDK providers: it reads the + * `localstack` profile's `endpoint_url` from `~/.aws/config`, falling back to + * the Toolkit's default when the profile is not configured. + */ +export async function getLocalStackEndpointUrl(): Promise { + try { + const contents = await readFile(AWS_CONFIG_FILENAME, "utf-8"); + const ini = parseIni(contents); + const section = ini.sections.find( + (s) => s.name === LOCALSTACK_PROFILE_SECTION, + ); + const url = section?.properties.endpoint_url; + if (url) { + return url; + } + } catch { + /* config missing or unreadable: fall back to the default */ + } + return DEFAULT_ENDPOINT; +} + +/** + * Reduce an endpoint URL to its `host:port` (or just host) for display. + */ +export function endpointHostPort(url: string): string { + try { + const parsed = new URL(url); + return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname; + } catch { + return url; + } +} diff --git a/src/utils/manage.ts b/src/utils/manage.ts index 376fa92..5a2ed81 100644 --- a/src/utils/manage.ts +++ b/src/utils/manage.ts @@ -161,7 +161,7 @@ export async function openLicensePage() { const url = new URL("https://app.localstack.cloud/settings/auth-tokens"); const openSuccessful = await env.openExternal(Uri.parse(url.toString())); if (!openSuccessful) { - window.showErrorMessage( + void window.showErrorMessage( `Open LocalStack License page in browser by entering the URL manually: ${url.toString()}`, ); } diff --git a/src/utils/memoize.ts b/src/utils/memoize.ts new file mode 100644 index 0000000..df26fe4 --- /dev/null +++ b/src/utils/memoize.ts @@ -0,0 +1,69 @@ +/** + * A memoized function, with a `clear()` to drop its cached results. + */ +export type Memoized = (( + ...args: Args +) => Result) & { clear: () => void }; + +/* + * Every memoized function's `clear` is registered here so all caches can be + * dropped at once — e.g. when the user refreshes to re-query AWS, or when + * credentials/endpoints may have changed and cached clients/results are stale. + */ +const registry = new Set<() => void>(); + +/** Drop every memoized cache (cached SDK clients and fetched data alike). */ +export function clearMemoizedCaches(): void { + for (const clear of registry) { + clear(); + } +} + +/** + * Memoizes a function's results based on its arguments. + * + * For promise-returning functions the *promise* is cached (not just its + * resolved value), so concurrent callers share a single in-flight request + * instead of each firing their own. A rejected promise is evicted so the + * failure is not served forever and the next call retries. + * + * Note: keys are derived via `JSON.stringify(args)`, so this is intended for + * primitive arguments (strings/numbers). Object args with differing key order + * — or non-serializable values — will not key reliably. + * + * @param func The function to memoize. + * @returns A memoized version of `func`, with a `clear()` to drop its cache. + */ +export function memoize( + func: (...args: Args) => Result, +): Memoized { + const cache = new Map(); + + const memoized = ((...args: Args): Result => { + const key = JSON.stringify(args); + if (cache.has(key)) { + return cache.get(key) as Result; + } + + const result = func(...args); + cache.set(key, result); + + if (result instanceof Promise) { + /* Don't let a rejection stick in the cache. Evict only if this exact + * promise is still the cached entry (a clear/refresh may have replaced + * it). The caller handles the rejection on its own copy of the promise; + * this handler exists solely to evict. */ + result.catch(() => { + if (cache.get(key) === result) { + cache.delete(key); + } + }); + } + + return result; + }) as Memoized; + + memoized.clear = () => cache.clear(); + registry.add(memoized.clear); + return memoized; +} diff --git a/src/views/explore/commands.ts b/src/views/explore/commands.ts new file mode 100644 index 0000000..6c4b45e --- /dev/null +++ b/src/views/explore/commands.ts @@ -0,0 +1,387 @@ +/* + * Command handlers for the Cloud Profiles affordances: add/remove regions and + * add/edit/remove views. Registered by the resource-browser plugin. + */ +import { commands, window } from "vscode"; +import type { Disposable, QuickPickItem } from "vscode"; + +import AWSConfig from "../../platforms/aws/models/awsConfig.ts"; +import { + getAllRegionCodes, + getRegionLongName, +} from "../../platforms/aws/models/regionModel.ts"; +import { ProviderFactory } from "../../platforms/aws/services/providerFactory.ts"; + +import { + getAddedRegions, + getInstanceViews, + getProfileViews, + removeInstanceView, + removeProfileView, + removeRegion, + resolveShownProfiles, + saveInstanceView, + saveProfileView, + setAddedRegions, + setShownProfiles, +} from "./settings.ts"; +import type { SavedView, ViewScope } from "./settings.ts"; +import type { + InstanceViewTreeItem, + ProfileViewTreeItem, + RegionTreeItem, +} from "./treeItems.ts"; +import type { LocalStackViewProvider } from "./viewProvider.ts"; + +/** + * Register the region/view CRUD commands; returns their disposables. + * `refreshResources` re-renders the Resources view after a view is + * added/edited/removed, so an active view reflects the change (or clears when + * removed) without the user reselecting it. + */ +export function registerLocalStackCommands( + provider: LocalStackViewProvider, + refreshResources: () => void, +): Disposable[] { + return [ + commands.registerCommand( + "localstack.addRegion", + (arg: { profileName: string }) => onAddRegion(provider, arg.profileName), + ), + commands.registerCommand("localstack.manageProfiles", () => + onManageProfiles(provider), + ), + commands.registerCommand( + "localstack.removeRegion", + (item: RegionTreeItem) => + onRemoveRegion(provider, item.profileName, item.regionId), + ), + commands.registerCommand( + "localstack.addProfileView", + (item: RegionTreeItem) => + onAddProfileView( + provider, + refreshResources, + item.profileName, + item.regionId, + ), + ), + commands.registerCommand( + "localstack.editProfileView", + (item: ProfileViewTreeItem) => + onEditProfileView(provider, refreshResources, item), + ), + commands.registerCommand( + "localstack.removeProfileView", + (item: ProfileViewTreeItem) => + onRemoveProfileView( + provider, + refreshResources, + item.profileName, + item.view.name, + ), + ), + commands.registerCommand("localstack.addInstanceView", () => + onAddInstanceView(provider, refreshResources), + ), + commands.registerCommand( + "localstack.editInstanceView", + (item: InstanceViewTreeItem) => + onEditInstanceView(provider, refreshResources, item), + ), + commands.registerCommand( + "localstack.removeInstanceView", + (item: InstanceViewTreeItem) => + onRemoveInstanceView(provider, refreshResources, item.view.name), + ), + ]; +} + +async function onAddInstanceView( + provider: LocalStackViewProvider, + refreshResources: () => void, +): Promise { + const result = await runViewWizard(undefined, undefined, undefined, true); + if (!result) { + return; + } + await saveInstanceView(result); + provider.refresh(); + refreshResources(); +} + +async function onEditInstanceView( + provider: LocalStackViewProvider, + refreshResources: () => void, + item: InstanceViewTreeItem, +): Promise { + const result = await runViewWizard(undefined, undefined, item.view, true); + if (!result) { + return; + } + await saveInstanceView(result, item.view.name); + provider.refresh(); + refreshResources(); +} + +async function onRemoveInstanceView( + provider: LocalStackViewProvider, + refreshResources: () => void, + name: string, +): Promise { + const confirmed = await window.showWarningMessage( + `Remove view "${name}" from the LocalStack instance?`, + { modal: true }, + "Remove", + ); + if (confirmed !== "Remove") { + return; + } + await removeInstanceView(name); + provider.refresh(); + refreshResources(); +} + +async function onAddRegion( + provider: LocalStackViewProvider, + profile: string, +): Promise { + const added = new Set(getAddedRegions(profile)); + /* The profile's default region is always shown and is not user-selectable. */ + const defaultRegion = AWSConfig.getRegionForProfile(profile); + + const items: QuickPickItem[] = getAllRegionCodes() + .filter((code) => code !== defaultRegion) + .map((code) => ({ + label: code, + description: safeLongName(code), + picked: added.has(code), + })); + + if (items.length === 0) { + void window.showInformationMessage("No additional regions are available."); + return; + } + + const picked = await window.showQuickPick(items, { + title: `Select Regions for "${profile}"`, + placeHolder: "Select the regions to show (deselect to hide)", + canPickMany: true, + }); + /* Undefined means cancelled; an empty array is a valid "hide all". */ + if (picked === undefined) { + return; + } + await setAddedRegions( + profile, + picked.map((p) => p.label), + ); + provider.refresh(); +} + +/** Toggle which Cloud Profiles are shown (does not touch ~/.aws/config). */ +async function onManageProfiles( + provider: LocalStackViewProvider, +): Promise { + const all = AWSConfig.getProfileNames(); + const shown = new Set(resolveShownProfiles(all)); + const items: QuickPickItem[] = all.map((profile) => ({ + label: profile, + picked: shown.has(profile), + })); + + if (items.length === 0) { + void window.showInformationMessage("No AWS profiles were found."); + return; + } + + const picked = await window.showQuickPick(items, { + title: "Select Profiles", + placeHolder: "Select the profiles to show (deselect to hide)", + canPickMany: true, + }); + if (picked === undefined) { + return; + } + await setShownProfiles(picked.map((p) => p.label)); + provider.refresh(); +} + +async function onRemoveRegion( + provider: LocalStackViewProvider, + profile: string, + region: string, +): Promise { + const confirmed = await window.showWarningMessage( + `Remove region "${region}" from profile "${profile}"?`, + { modal: true }, + "Remove", + ); + if (confirmed !== "Remove") { + return; + } + await removeRegion(profile, region); + provider.refresh(); +} + +async function onAddProfileView( + provider: LocalStackViewProvider, + refreshResources: () => void, + profile: string, + region: string, +): Promise { + const result = await runViewWizard(profile, region); + if (!result) { + return; + } + await saveProfileView(profile, result); + provider.refresh(); + refreshResources(); +} + +async function onEditProfileView( + provider: LocalStackViewProvider, + refreshResources: () => void, + item: ProfileViewTreeItem, +): Promise { + const region = + "region" in item.view.scope ? item.view.scope.region : undefined; + const result = await runViewWizard(item.profileName, region, item.view); + if (!result) { + return; + } + await saveProfileView(item.profileName, result, item.view.name); + provider.refresh(); + refreshResources(); +} + +async function onRemoveProfileView( + provider: LocalStackViewProvider, + refreshResources: () => void, + profile: string, + name: string, +): Promise { + const confirmed = await window.showWarningMessage( + `Remove view "${name}" from profile "${profile}"?`, + { modal: true }, + "Remove", + ); + if (confirmed !== "Remove") { + return; + } + await removeProfileView(profile, name); + provider.refresh(); + refreshResources(); +} + +/** + * The view wizard: name -> services -> scope. Returns the new view, or + * undefined if the user cancelled at any step. `existing` pre-populates the + * fields for an edit. For an instance view (`isInstance`), the scope step is + * skipped (instance views always apply to the running instance) and the name + * must be unique among instance views rather than a profile's views. + */ +async function runViewWizard( + profile: string | undefined, + region: string | undefined, + existing?: SavedView, + isInstance = false, +): Promise { + /* Step 1: name (unique within the profile, or among instance views). */ + const siblings = isInstance + ? getInstanceViews() + : getProfileViews(profile ?? ""); + const takenNames = new Set( + siblings.map((v) => v.name).filter((n) => n !== existing?.name), + ); + const scopeLabel = isInstance ? "the instance" : "the profile"; + const name = await window.showInputBox({ + title: "View name", + value: existing?.name, + prompt: `A name for this view (must be unique within ${scopeLabel})`, + validateInput: (value) => { + const trimmed = value.trim(); + if (!trimmed) { + return "Name cannot be empty"; + } + if (trimmed.toLowerCase() === "all resources") { + return `"All Resources" is a reserved view name`; + } + if (takenNames.has(trimmed)) { + return `A view named "${trimmed}" already exists in ${scopeLabel}`; + } + return undefined; + }, + }); + if (name === undefined) { + return undefined; + } + + /* Step 2: service/resource-type pairs (at least one). */ + const resourceItems: (QuickPickItem & { + service: string; + resourceType: string; + })[] = ProviderFactory.getSupportedServices().flatMap((p) => + p.getResourceTypes().map((resourceType) => { + const [, pluralName] = p.getResourceTypeNames(resourceType); + return { + label: `${p.getName()} — ${pluralName}`, + service: p.getId(), + resourceType, + picked: + existing?.resources.some( + (r) => r.service === p.getId() && r.resourceType === resourceType, + ) ?? false, + }; + }), + ); + const pickedResources = await window.showQuickPick(resourceItems, { + title: "Select resource types for this view", + placeHolder: "Pick one or more service resource types", + canPickMany: true, + }); + if (!pickedResources || pickedResources.length === 0) { + return undefined; + } + + /* Step 3: scope. Instance views always apply to the running instance, so the + * scope step is skipped and a placeholder scope is stored (unused there). */ + let scope: ViewScope = { allRegions: true }; + if (!isInstance) { + const thisRegionLabel = region + ? `This region only (${region})` + : "This region only"; + const allRegionsLabel = "All regions in this profile"; + const scopeChoice = await window.showQuickPick( + [thisRegionLabel, allRegionsLabel], + { title: "Where should this view appear?" }, + ); + if (!scopeChoice) { + return undefined; + } + if (scopeChoice === allRegionsLabel) { + scope = { allRegions: true }; + } else if (region) { + scope = { region }; + } else { + scope = { allRegions: true }; + } + } + + return { + name: name.trim(), + resources: pickedResources.map((r) => ({ + service: r.service, + resourceType: r.resourceType, + })), + scope, + }; +} + +function safeLongName(code: string): string { + try { + return getRegionLongName(code); + } catch { + return ""; + } +} diff --git a/src/views/explore/settings.ts b/src/views/explore/settings.ts new file mode 100644 index 0000000..e44f58e --- /dev/null +++ b/src/views/explore/settings.ts @@ -0,0 +1,258 @@ +/* + * Read/write helpers for the per-workspace configuration backing the + * Cloud Profiles section of the Explore view: user-added regions and + * user-defined views. All state lives in VS Code workspace settings so it is + * visible/editable in settings.json and survives reloads. + */ +import { ConfigurationTarget, workspace } from "vscode"; + +const CONFIG_SECTION = "localstack"; +const REGIONS_KEY = "cloudProfiles.regions"; +const PROFILE_VIEWS_KEY = "cloudProfiles.views"; +const SHOWN_PROFILES_KEY = "cloudProfiles.shown"; +/* Instance views are stored separately from cloud-profile views so they never + * collide with the bundled `localstack` cloud profile's region views. */ +const INSTANCE_VIEWS_KEY = "instanceViews"; + +/** + * Deep-clone plain JSON config data. We cannot use `structuredClone` on the + * objects returned by `workspace.getConfiguration().get()` — they are not + * structured-cloneable and throw `# could not be cloned`. + */ +function cloneJson(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +/** + * Choose where to persist settings: per-workspace when a folder is open, + * global otherwise. Writing to `Workspace` with no folder open throws + * "Unable to write to Workspace Settings because no workspace is opened". + */ +export function configTarget(): ConfigurationTarget { + return workspace.workspaceFolders?.length + ? ConfigurationTarget.Workspace + : ConfigurationTarget.Global; +} + +/** Scope of a saved view: a single region, or every region of the profile. */ +export type ViewScope = { region: string } | { allRegions: true }; + +/** A service and one of its resource types, the granularity a view filters at. */ +export interface ResourcePair { + service: string; + resourceType: string; +} + +export interface SavedView { + name: string; + resources: ResourcePair[]; + scope: ViewScope; +} + +type RegionsMap = Record; +type ProfileViewsMap = Record; + +function regionsMap(): RegionsMap { + return ( + workspace.getConfiguration(CONFIG_SECTION).get(REGIONS_KEY) ?? + {} + ); +} + +function profileViewsMap(): ProfileViewsMap { + return ( + workspace + .getConfiguration(CONFIG_SECTION) + .get(PROFILE_VIEWS_KEY) ?? {} + ); +} + +/** User-added regions for a profile (excludes the profile's default region). */ +export function getAddedRegions(profile: string): string[] { + return regionsMap()[profile] ?? []; +} + +export async function addRegion( + profile: string, + region: string, +): Promise { + const map = cloneJson(regionsMap()); + const list = map[profile] ?? []; + if (!list.includes(region)) { + list.push(region); + } + map[profile] = list; + await workspace + .getConfiguration(CONFIG_SECTION) + .update(REGIONS_KEY, map, configTarget()); +} + +export async function removeRegion( + profile: string, + region: string, +): Promise { + const map = cloneJson(regionsMap()); + map[profile] = (map[profile] ?? []).filter((r) => r !== region); + if (map[profile].length === 0) { + delete map[profile]; + } + await workspace + .getConfiguration(CONFIG_SECTION) + .update(REGIONS_KEY, map, configTarget()); +} + +/** Replace the full set of user-added regions for a profile. */ +export async function setAddedRegions( + profile: string, + regions: string[], +): Promise { + const map = cloneJson(regionsMap()); + if (regions.length === 0) { + delete map[profile]; + } else { + map[profile] = regions; + } + await workspace + .getConfiguration(CONFIG_SECTION) + .update(REGIONS_KEY, map, configTarget()); +} + +/** + * Profile names the user has chosen to show under Cloud Profiles. Returns + * `undefined` when unset (never configured), which is distinct from an empty + * list (the user explicitly chose to show nothing). + */ +export function getShownProfiles(): string[] | undefined { + /* Use inspect() rather than get(): a contributed array setting always + * resolves to its default ([]) via get(), which would erase the + * unset-vs-empty distinction. Only an explicit user value at any scope + * counts as "set". */ + const inspected = workspace + .getConfiguration(CONFIG_SECTION) + .inspect(SHOWN_PROFILES_KEY); + return ( + inspected?.workspaceFolderValue ?? + inspected?.workspaceValue ?? + inspected?.globalValue + ); +} + +/** Replace the set of shown profile names. */ +export async function setShownProfiles(profiles: string[]): Promise { + await workspace + .getConfiguration(CONFIG_SECTION) + .update(SHOWN_PROFILES_KEY, profiles, configTarget()); +} + +/** The default shown set when unset: `default` if present, else the first. */ +export function defaultShownProfiles(allProfiles: string[]): string[] { + if (allProfiles.includes("default")) { + return ["default"]; + } + return allProfiles.length > 0 ? [allProfiles[0]] : []; +} + +/** + * The effective set of shown profile names: the configured set when set + * (including an explicit empty list), otherwise the default-only fallback. + */ +export function resolveShownProfiles(allProfiles: string[]): string[] { + return getShownProfiles() ?? defaultShownProfiles(allProfiles); +} + +/** All views defined for a profile. */ +export function getProfileViews(profile: string): SavedView[] { + return profileViewsMap()[profile] ?? []; +} + +/** Views that apply to a given region: its own plus the profile's all-region views. */ +export function getProfileViewsForRegion( + profile: string, + region: string, +): SavedView[] { + return getProfileViews(profile).filter((v) => + "allRegions" in v.scope ? v.scope.allRegions : v.scope.region === region, + ); +} + +/** + * Add a new view or overwrite an existing one. When `originalName` is given + * (an edit), the view with that name is replaced; otherwise the view is + * appended (or replaces one with the same name). + */ +export async function saveProfileView( + profile: string, + view: SavedView, + originalName?: string, +): Promise { + const map = cloneJson(profileViewsMap()); + const list = map[profile] ?? []; + const key = originalName ?? view.name; + const index = list.findIndex((v) => v.name === key); + if (index >= 0) { + list[index] = view; + } else { + list.push(view); + } + map[profile] = list; + await workspace + .getConfiguration(CONFIG_SECTION) + .update(PROFILE_VIEWS_KEY, map, configTarget()); +} + +export async function removeProfileView( + profile: string, + name: string, +): Promise { + const map = cloneJson(profileViewsMap()); + map[profile] = (map[profile] ?? []).filter((v) => v.name !== name); + if (map[profile].length === 0) { + delete map[profile]; + } + await workspace + .getConfiguration(CONFIG_SECTION) + .update(PROFILE_VIEWS_KEY, map, configTarget()); +} + +/* ── Instance views ─────────────────────────────────────────────────────── + * Saved views for the running LocalStack instance. They reuse the SavedView + * shape (with a placeholder scope that instance rendering ignores) but live + * under their own key, separate from cloud-profile views. */ + +function instanceViews(): SavedView[] { + return ( + workspace + .getConfiguration(CONFIG_SECTION) + .get(INSTANCE_VIEWS_KEY) ?? [] + ); +} + +/** All saved views for the LocalStack instance. */ +export function getInstanceViews(): SavedView[] { + return instanceViews(); +} + +/** Add a new instance view, or overwrite one by name (`originalName` on edit). */ +export async function saveInstanceView( + view: SavedView, + originalName?: string, +): Promise { + const list = cloneJson(instanceViews()); + const key = originalName ?? view.name; + const index = list.findIndex((v) => v.name === key); + if (index >= 0) { + list[index] = view; + } else { + list.push(view); + } + await workspace + .getConfiguration(CONFIG_SECTION) + .update(INSTANCE_VIEWS_KEY, list, configTarget()); +} + +export async function removeInstanceView(name: string): Promise { + const list = instanceViews().filter((v) => v.name !== name); + await workspace + .getConfiguration(CONFIG_SECTION) + .update(INSTANCE_VIEWS_KEY, list, configTarget()); +} diff --git a/src/views/explore/treeItems.ts b/src/views/explore/treeItems.ts new file mode 100644 index 0000000..027faf0 --- /dev/null +++ b/src/views/explore/treeItems.ts @@ -0,0 +1,157 @@ +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; + +import type { Focus } from "../../models/focus.ts"; +import type { LocalStackStatus } from "../../utils/localstack-status.ts"; + +import type { SavedView } from "./settings.ts"; + +/** Capitalize a status word for display, e.g. "stopped" → "Stopped". */ +function capitalizeStatus(status: LocalStackStatus): string { + return status.charAt(0).toUpperCase() + status.slice(1); +} + +/** Root class for every node in the LocalStack view. */ +export class LocalStackTreeItem extends TreeItem {} + +/** The three top-level sections. */ +export type SectionKind = "instances" | "profiles" | "workspace"; + +export class SectionTreeItem extends LocalStackTreeItem { + constructor( + public readonly kind: SectionKind, + label: string, + ) { + super(label, TreeItemCollapsibleState.Expanded); + this.contextValue = `localstackSection:${kind}`; + } +} + +/** + * A LocalStack emulator instance (LocalStack Instances section). The live + * status is shown inline on the label, e.g. `AWS (Stopped): localhost:4566`. + */ +export class InstanceTreeItem extends LocalStackTreeItem { + constructor( + public readonly hostPort: string, + status: LocalStackStatus, + ) { + super("", TreeItemCollapsibleState.Expanded); + this.contextValue = "localstackInstance"; + this.iconPath = new ThemeIcon("server-environment"); + this.setStatus(status); + } + + /** Update the inline status portion of the label and expandability. */ + setStatus(status: LocalStackStatus): void { + this.label = `AWS (${capitalizeStatus(status)}): ${this.hostPort}`; + /* Only a running instance has children (App Inspector, View selectors), + * so a stopped instance renders as a plain, non-expandable line. */ + this.collapsibleState = + status === "running" + ? TreeItemCollapsibleState.Expanded + : TreeItemCollapsibleState.None; + } +} + +/** Opens the App Inspector webview when clicked. Only shown while running. */ +export class AppInspectorTreeItem extends LocalStackTreeItem { + constructor() { + super("App Inspector", TreeItemCollapsibleState.None); + this.description = "Click to open"; + this.contextValue = "localstackAppInspector"; + this.iconPath = new ThemeIcon("search"); + this.command = { + title: "Open App Inspector", + command: "localstack.openAppInspector", + }; + } +} + +/** An AWS profile from ~/.aws/config (Cloud Profiles section). */ +export class ProfileTreeItem extends LocalStackTreeItem { + constructor(public readonly profileName: string) { + super(`AWS: ${profileName}`, TreeItemCollapsibleState.Collapsed); + this.contextValue = "localstackProfile"; + this.iconPath = new ThemeIcon("account"); + } +} + +/** A region under a profile. */ +export class RegionTreeItem extends LocalStackTreeItem { + constructor( + public readonly profileName: string, + public readonly regionId: string, + public readonly isDefault: boolean, + ) { + super(regionId, TreeItemCollapsibleState.Collapsed); + this.description = isDefault ? "(default)" : undefined; + /* Only user-added regions offer a Remove action. */ + this.contextValue = isDefault + ? "localstackDefaultRegion" + : "localstackUserRegion"; + } +} + +/** + * A focus selector: clicking it sets the active focus for the Resources view. + * `getFocus` is invoked lazily when the node is selected. + */ +export class FocusSelectorTreeItem extends LocalStackTreeItem { + constructor( + label: string, + public readonly getFocus: () => Promise, + ) { + super(label, TreeItemCollapsibleState.None); + this.contextValue = "localstackFocusSelector"; + this.tooltip = `Select to focus on: ${label}`; + /* Transparent icon so the label aligns with icon-bearing siblings — + * specifically the `App Inspector` node alongside the instance's + * `View: All Resources` selector. */ + this.iconPath = new ThemeIcon("blank"); + } +} + +/** A user-defined cloud-profile view focus selector (supports Edit/Remove). */ +export class ProfileViewTreeItem extends FocusSelectorTreeItem { + constructor( + public readonly profileName: string, + public readonly view: SavedView, + getFocus: () => Promise, + ) { + super(`View: ${view.name}`, getFocus); + this.contextValue = "localstackProfileView"; + } +} + +/** + * A user-defined view for the LocalStack instance (supports Edit/Remove). + * Distinct from `ProfileViewTreeItem` (cloud-profile views) so its menus and + * storage stay separate; its focus intersects the live metamodel with the + * chosen pairs. + */ +export class InstanceViewTreeItem extends FocusSelectorTreeItem { + constructor( + public readonly view: SavedView, + getFocus: () => Promise, + ) { + super(`View: ${view.name}`, getFocus); + this.contextValue = "localstackInstanceView"; + } +} + +/** A non-interactive placeholder (e.g. "Coming soon", "[ No stacks ]"). */ +export class PlaceholderTreeItem extends LocalStackTreeItem { + constructor(message: string) { + super(message, TreeItemCollapsibleState.None); + this.tooltip = message; + } +} + +/** An error node shown in place of content we could not load. */ +export class ErrorTreeItem extends LocalStackTreeItem { + constructor(message: string) { + super(`Error: ${message}`, TreeItemCollapsibleState.None); + this.tooltip = message; + this.iconPath = new ThemeIcon("error"); + } +} diff --git a/src/views/explore/viewProvider.ts b/src/views/explore/viewProvider.ts new file mode 100644 index 0000000..d9523f3 --- /dev/null +++ b/src/views/explore/viewProvider.ts @@ -0,0 +1,337 @@ +import { EventEmitter } from "vscode"; +import type { + Event, + LogOutputChannel, + ProviderResult, + TreeDataProvider, +} from "vscode"; + +import { makeWildcardFocus } from "../../models/focus.ts"; +import type { Focus } from "../../models/focus.ts"; +import { CloudFormation } from "../../platforms/aws/clients/cloudformation.ts"; +import ARN from "../../platforms/aws/models/arnModel.ts"; +import AWSConfig from "../../platforms/aws/models/awsConfig.ts"; +import CfnStackModel from "../../platforms/aws/models/cfnStackModel.ts"; +import { computeMetamodelFocus } from "../../platforms/aws/models/metamodelFocus.ts"; +import { + endpointHostPort, + getLocalStackEndpointUrl, +} from "../../utils/localstack-endpoint.ts"; +import type { LocalStackStatusTracker } from "../../utils/localstack-status.ts"; + +import { + getAddedRegions, + getInstanceViews, + getProfileViewsForRegion, + resolveShownProfiles, +} from "./settings.ts"; +import type { ResourcePair, SavedView } from "./settings.ts"; +import { + AppInspectorTreeItem, + ErrorTreeItem, + FocusSelectorTreeItem, + InstanceTreeItem, + InstanceViewTreeItem, + PlaceholderTreeItem, + ProfileTreeItem, + ProfileViewTreeItem, + RegionTreeItem, + SectionTreeItem, +} from "./treeItems.ts"; +import type { LocalStackTreeItem } from "./treeItems.ts"; + +/** + * Tree data provider for the combined "LocalStack" view, with three sections: + * LocalStack Instances, Cloud Profiles, and Workspace IaC. Leaf focus selectors + * drive the Resources view. + */ +export class LocalStackViewProvider + implements TreeDataProvider +{ + readonly #onDidChangeTreeData = new EventEmitter< + // biome-ignore lint/suspicious/noConfusingVoidType: required by the Event signature + LocalStackTreeItem | undefined | void + >(); + readonly onDidChangeTreeData: Event< + // biome-ignore lint/suspicious/noConfusingVoidType: required by the Event signature + LocalStackTreeItem | undefined | void + > = this.#onDidChangeTreeData.event; + + #instanceItem: InstanceTreeItem | undefined; + + constructor( + private readonly statusTracker: LocalStackStatusTracker, + private readonly log?: LogOutputChannel, + ) { + this.statusTracker.onChange((status) => { + if (this.#instanceItem) { + /* Update the inline status label and refresh the instance node; + * firing it also rebuilds its children so the App Inspector + * description reflects the new running state. */ + this.#instanceItem.setStatus(status); + this.#onDidChangeTreeData.fire(this.#instanceItem); + } + }); + } + + /** Refresh the whole tree (e.g. after a settings change). */ + refresh(): void { + this.#onDidChangeTreeData.fire(); + } + + getTreeItem(element: LocalStackTreeItem): LocalStackTreeItem { + return element; + } + + getChildren( + element?: LocalStackTreeItem, + ): ProviderResult { + if (!element) { + return [ + new SectionTreeItem("instances", "LocalStack Instances"), + new SectionTreeItem("profiles", "Cloud Profiles"), + new SectionTreeItem("workspace", "Workspace IaC"), + ]; + } + if (element instanceof SectionTreeItem) { + switch (element.kind) { + case "instances": + return this.makeInstances(); + case "profiles": + return this.makeProfiles(); + case "workspace": + return [new PlaceholderTreeItem("Coming soon")]; + } + } + if (element instanceof InstanceTreeItem) { + return this.makeInstanceChildren(); + } + if (element instanceof ProfileTreeItem) { + return this.makeProfileChildren(element.profileName); + } + if (element instanceof RegionTreeItem) { + return this.makeRegionChildren(element.profileName, element.regionId); + } + return []; + } + + /** + * Compute the focus for the current selection. The view is single-select, so + * at most one focus selector is active; return its focus (or undefined when + * the selection contains no focus selector). + */ + async computeFocus( + selection: readonly LocalStackTreeItem[], + ): Promise { + const selector = selection.find( + (item): item is FocusSelectorTreeItem => + item instanceof FocusSelectorTreeItem, + ); + return selector ? selector.getFocus() : undefined; + } + + private async makeInstances(): Promise { + const endpoint = await getLocalStackEndpointUrl(); + const item = new InstanceTreeItem( + endpointHostPort(endpoint), + this.statusTracker.status(), + ); + this.#instanceItem = item; + return [item]; + } + + private makeInstanceChildren(): LocalStackTreeItem[] { + /* Children are only meaningful while the emulator is running. */ + if (this.statusTracker.status() !== "running") { + return []; + } + + const allResources = new FocusSelectorTreeItem( + "View: All Resources", + async () => { + const endpoint = await getLocalStackEndpointUrl(); + return computeMetamodelFocus(endpoint, this.log); + }, + ); + + /* Saved instance views: the metamodel focus narrowed to the view's chosen + * pairs. Resolved live by name so edits/removes propagate on refresh. */ + const views = getInstanceViews().map( + (view) => + new InstanceViewTreeItem(view, async () => { + const endpoint = await getLocalStackEndpointUrl(); + const metamodel = await computeMetamodelFocus(endpoint, this.log); + const live = getInstanceViews().find((v) => v.name === view.name); + return live + ? intersectMetamodelWithPairs(metamodel, live.resources) + : undefined; + }), + ); + + return [new AppInspectorTreeItem(), allResources, ...views]; + } + + private makeProfiles(): LocalStackTreeItem[] { + const all = AWSConfig.getProfileNames(); + const shown = new Set(resolveShownProfiles(all)); + const profiles = all.filter((profile) => shown.has(profile)); + if (profiles.length === 0) { + return [new PlaceholderTreeItem("No profiles selected")]; + } + return profiles.map((profile) => new ProfileTreeItem(profile)); + } + + private makeProfileChildren(profile: string): LocalStackTreeItem[] { + const regions: LocalStackTreeItem[] = []; + const defaultRegion = AWSConfig.getRegionForProfile(profile); + const seen = new Set(); + if (defaultRegion) { + regions.push(new RegionTreeItem(profile, defaultRegion, true)); + seen.add(defaultRegion); + } + for (const region of getAddedRegions(profile)) { + if (!seen.has(region)) { + regions.push(new RegionTreeItem(profile, region, false)); + seen.add(region); + } + } + return regions; + } + + private async makeRegionChildren( + profile: string, + region: string, + ): Promise { + const children: LocalStackTreeItem[] = []; + + children.push( + new FocusSelectorTreeItem("View: All Resources", () => + Promise.resolve(makeWildcardFocus(profile, region)), + ), + ); + + for (const view of getProfileViewsForRegion(profile, region)) { + children.push( + new ProfileViewTreeItem(profile, view, () => + /* Resolve the view live so an edit to the active view is + * reflected on refresh, and a removed view yields no focus + * (clearing the Resources view) rather than stale content. */ + Promise.resolve( + resolveRegionViewFocus( + profile, + region, + view.name, + getProfileViewsForRegion(profile, region), + ), + ), + ), + ); + } + + /* CloudFormation stacks for this profile/region. */ + try { + const stacks = await CloudFormation.listStacks(profile, region); + for (const stack of stacks) { + const stackId = stack.StackId; + if (!stackId) { + continue; + } + children.push( + new FocusSelectorTreeItem(`Stack: ${stack.StackName}`, () => + new CfnStackModel( + profile, + new ARN(stackId), + this.log, + ).toFocusModel(), + ), + ); + } + } catch (error) { + children.push( + new ErrorTreeItem( + `Could not list CloudFormation stacks: ${String(error)}`, + ), + ); + } + + return children; + } +} + +/** + * Resolve a region view's focus live from the given view list, by name. + * Returns `undefined` when no view with that name exists (e.g. it was removed + * or renamed), so the Resources view clears rather than showing stale content. + * Pure (the view list is passed in) so the live-resolution behavior is + * testable; the caller passes the current `getProfileViewsForRegion(...)` result. + */ +export function resolveRegionViewFocus( + profile: string, + region: string, + name: string, + views: SavedView[], +): Focus | undefined { + const live = views.find((v) => v.name === name); + return live ? makeProfileViewFocus(profile, region, live) : undefined; +} + +/** + * Narrow a metamodel-derived focus to a set of chosen service/resource-type + * pairs (an instance view). Keeps only the pairs that are actually present in + * the metamodel, so a view never lists a type with nothing deployed; services + * and regions left empty are dropped. Exported for testing. + */ +export function intersectMetamodelWithPairs( + metamodel: Focus, + pairs: ResourcePair[], +): Focus { + const wantedServices = new Set(pairs.map((p) => p.service)); + const wantedPairs = new Set( + pairs.map((p) => `${p.service}${p.resourceType}`), + ); + const profile = metamodel.profiles[0]; + const regions = (profile?.regions ?? []) + .map((region) => ({ + id: region.id, + services: region.services + .filter((s) => wantedServices.has(s.id)) + .map((s) => ({ + id: s.id, + resourcetypes: s.resourcetypes.filter((rt) => + wantedPairs.has(`${s.id}${rt.id}`), + ), + })) + .filter((s) => s.resourcetypes.length > 0), + })) + .filter((region) => region.services.length > 0); + + return { + version: "1.0", + profiles: [{ id: profile?.id ?? "localstack", regions }], + }; +} + +/** Build a focus for a region scoped to a view's chosen service/type pairs. */ +function makeProfileViewFocus( + profile: string, + region: string, + view: SavedView, +): Focus { + /* Group the flat pair list by service into the focus shape. */ + const resourceTypesByService = new Map(); + for (const { service, resourceType } of view.resources) { + const list = resourceTypesByService.get(service) ?? []; + list.push(resourceType); + resourceTypesByService.set(service, list); + } + const services = [...resourceTypesByService.entries()].map( + ([id, resourceTypes]) => ({ + id, + resourcetypes: resourceTypes.map((rt) => ({ id: rt, arns: ["*"] })), + }), + ); + return { + version: "1.0", + profiles: [{ id: profile, regions: [{ id: region, services }] }], + }; +} diff --git a/src/views/resource-details/viewProvider.ts b/src/views/resource-details/viewProvider.ts new file mode 100644 index 0000000..a21a6a3 --- /dev/null +++ b/src/views/resource-details/viewProvider.ts @@ -0,0 +1,226 @@ +import { commands } from "vscode"; +import type * as vscode from "vscode"; + +import ARN from "../../platforms/aws/models/arnModel.ts"; +import { ProviderFactory } from "../../platforms/aws/services/providerFactory.ts"; +import { FieldType } from "../../platforms/aws/services/serviceProvider.ts"; + +/** A single described field, as returned by a provider's `describeResource`. */ +interface DetailField { + field: string; + value: string; + type: FieldType; +} + +const HTML_ESCAPES: Record = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", +}; + +function escapeHtml(value: string): string { + return value.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char] ?? char); +} + +/** Pretty-print a JSON string; fall back to the raw text if it does not parse. */ +function prettyJson(value: string): string { + try { + return JSON.stringify(JSON.parse(value) as unknown, null, 2); + } catch { + return value; + } +} + +/* Inline stylesheet — themed via VS Code CSS variables so it tracks the theme. */ +const STYLES = ` + body { + color: var(--vscode-foreground); + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + padding: 0 8px 8px; + } + table { + border-collapse: collapse; + width: 100%; + /* Fixed layout makes the column widths below hard caps, so a long field + * label wraps within its 20% column instead of stretching it. */ + table-layout: fixed; + } + td { + padding: 3px 8px; + vertical-align: top; + border-bottom: 1px solid var(--vscode-widget-border, transparent); + } + td.field { + color: var(--vscode-descriptionForeground); + width: 20%; + white-space: normal; + overflow-wrap: break-word; + } + td.value { + width: 80%; + word-break: break-word; + } + .mono { + font-family: var(--vscode-editor-font-family); + font-size: var(--vscode-editor-font-size); + } + pre.block { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-family: var(--vscode-editor-font-family); + font-size: var(--vscode-editor-font-size); + } + .placeholder { + color: var(--vscode-descriptionForeground); + } + .error { + color: var(--vscode-errorForeground); + } +`; + +/** + * Provider for the "Resource Details" webview, showing a themed key/value table + * for the AWS resource currently selected in the "Resources" view. Rendered as a + * webview (rather than a tree) so values can be laid out as a real table and + * formatted per field type. The content changes whenever a new resource is + * selected, and can be refreshed manually. + */ +export class ResourceDetailsViewProvider implements vscode.WebviewViewProvider { + /** The resolved webview view, once VS Code has created it. */ + private view: vscode.WebviewView | undefined; + + /** The ARN of the currently selected resource. */ + private arn: string | undefined = undefined; + + /** The profile of the currently selected resource. */ + private profile: string | undefined = undefined; + + resolveWebviewView(webviewView: vscode.WebviewView): void { + this.view = webviewView; + /* No scripts are needed: the table is static HTML. */ + webviewView.webview.options = { enableScripts: false }; + this.render(); + } + + /** Update the view to show details of the selected resource. */ + public setArn(profile: string, arn: string): void { + if (arn !== this.arn || profile !== this.profile) { + this.arn = arn; + this.profile = profile; + this.render(); + } + } + + /** Re-fetch and re-render the currently selected resource (manual refresh). */ + public refresh(): void { + this.render(); + } + + /** + * Bring the Resource Details panel forward without stealing keyboard focus, + * so the user can keep arrow-key browsing the Resources tree while details + * follow live. Before the panel has ever been opened the view isn't resolved + * yet, so fall back to the auto-generated focus command to open it the first + * time (that one reveal does take focus). + */ + public reveal(): void { + if (this.view) { + this.view.show(true); // preserveFocus: keep focus in the Resources tree + } else { + void commands.executeCommand("localstack.resourceDetails.focus"); + } + } + + /** Render the current state into the webview (no-op until it is resolved). */ + private render(): void { + if (!this.view) { + return; + } + if (!this.arn || !this.profile) { + this.view.webview.html = this.htmlDocument( + `

Please select a resource in the Resources view.

`, + ); + return; + } + this.view.webview.html = this.htmlDocument( + `

Loading…

`, + ); + void this.renderResource(this.profile, this.arn); + } + + /** Fetch the resource's fields and render them, guarding against stale fetches. */ + private async renderResource(profile: string, arn: string): Promise { + let body: string; + try { + const resourceArn = new ARN(arn); + const service = ProviderFactory.getProviderForService( + resourceArn.service, + ); + const fields = await service.describeResource(profile, resourceArn); + const rows: DetailField[] = [ + { field: "ARN", value: arn, type: FieldType.ARN }, + { field: "Service", value: service.getName(), type: FieldType.NAME }, + ...fields, + ]; + body = this.tableBody(rows); + } catch (error) { + body = `

Could not load resource details: ${escapeHtml( + String(error), + )}

`; + } + /* A newer selection (or refresh) may have superseded this fetch. */ + if (!this.view || this.arn !== arn || this.profile !== profile) { + return; + } + this.view.webview.html = this.htmlDocument(body); + } + + private tableBody(rows: DetailField[]): string { + const trs = rows + .map( + (row) => + `${escapeHtml( + row.field, + )}${this.valueCell( + row.value, + row.type, + )}`, + ) + .join(""); + return `${trs}
`; + } + + /** Render a value cell, formatted according to its field type. */ + private valueCell(value: string, type: FieldType): string { + switch (type) { + case FieldType.JSON: + return `
${escapeHtml(prettyJson(value))}
`; + case FieldType.LONG_TEXT: + return `
${escapeHtml(value)}
`; + case FieldType.ARN: + case FieldType.LOG_GROUP: + case FieldType.NUMBER: + case FieldType.DATE: + return `${escapeHtml(value)}`; + default: + return escapeHtml(value); + } + } + + private htmlDocument(body: string): string { + return ` + + + + + + + +${body} +`; + } +} diff --git a/src/views/resources/treeItems.ts b/src/views/resources/treeItems.ts new file mode 100644 index 0000000..25af4cb --- /dev/null +++ b/src/views/resources/treeItems.ts @@ -0,0 +1,120 @@ +import * as vscode from "vscode"; + +import type { + ProfileFocus, + RegionFocus, + ResourceTypeFocus, + ServiceFocus, +} from "../../models/focus.ts"; +import type { ServiceProvider } from "../../platforms/aws/services/serviceProvider.ts"; + +/** + * The top-level class for any TreeItem in the Resources View + */ +export class ResourceTreeItem extends vscode.TreeItem { + constructor( + public readonly label: string, + state?: vscode.TreeItemCollapsibleState, + ) { + super(label, state); + } +} + +/** + * Represents a TreeItem for a Profile + * @param profile The ProfileFocus object providing Profile details. + * @param accountId The AWS account ID associated with the profile. + * @param accountName The name of the AWS account associated with the profile. + * @param isLocalStack Whether this profile targets a LocalStack emulator (vs real AWS). + * Determines the target-aware icon shown on the service rows beneath it. + */ +export class ResourceProfileTreeItem extends ResourceTreeItem { + constructor( + public readonly profile: ProfileFocus, + public readonly accountId: string, + public readonly accountName: string, + public readonly isLocalStack: boolean, + ) { + super(`Profile: ${profile.id}`, vscode.TreeItemCollapsibleState.Expanded); + /* Only render the alias (and its separator) when one is set, otherwise + * the description shows a dangling "( - )". */ + this.description = accountName + ? `(${accountId} - ${accountName})` + : `(${accountId})`; + } +} + +/** + * Represents a TreeItem for a Region + */ +export class ResourceRegionTreeItem extends ResourceTreeItem { + constructor( + public readonly parent: ResourceProfileTreeItem, + public readonly region: RegionFocus, + public readonly locationName: string, + ) { + super(region.id, vscode.TreeItemCollapsibleState.Collapsed); + this.description = locationName; + } +} + +/** + * Represents a single row combining a service and one of its resource types, + * e.g. label `SQS` with the dimmed description `Queues`. A service with several + * resource types yields several of these rows (sharing the service name/icon). + */ +export class ResourceServiceTypeTreeItem extends ResourceTreeItem { + constructor( + public readonly parent: ResourceRegionTreeItem, + public readonly service: ServiceFocus, + public readonly provider: ServiceProvider, + public readonly resourceType: ResourceTypeFocus, + ) { + super(provider.getName(), vscode.TreeItemCollapsibleState.Collapsed); + const [, pluralName] = provider.getResourceTypeNames(resourceType.id); + /* Resource type shown as dimmed description after the service name. */ + this.description = pluralName; + /* The icon denotes the profile's target (LocalStack vs AWS), not the + * service. It lives on this combined row, not on the resource leaves. */ + this.iconPath = new vscode.ThemeIcon( + parent.parent.isLocalStack ? "localstack-logo" : "cloud", + ); + } +} + +/** + * Represents a TreeItem for a Resource + */ +export class ResourceArnTreeItem extends ResourceTreeItem { + constructor( + public readonly parent: ResourceServiceTypeTreeItem, + public readonly arn: string, + public readonly name: string, + public readonly tooltip: string, + ) { + super(name); + } +} + +/** + * Represents a TreeItem that we weren't able to show because + * of some error. + */ +export class ResourceErrorTreeItem extends ResourceTreeItem { + constructor(public readonly errorMessage: string) { + super(`Error: ${errorMessage}`, vscode.TreeItemCollapsibleState.None); + this.tooltip = errorMessage; + this.iconPath = new vscode.ThemeIcon("error"); + } +} + +/** + * Represents a TreeItem that is essentially a placeholder. This is not an + * error, but more for indicating that there are no resources to display. + */ +export class ResourcePlaceholderTreeItem extends ResourceTreeItem { + constructor(message: string = "[ No Resources ]") { + super(message, vscode.TreeItemCollapsibleState.None); + this.tooltip = "No Resources to Display"; + } +} diff --git a/src/views/resources/viewProvider.ts b/src/views/resources/viewProvider.ts new file mode 100644 index 0000000..79ca34a --- /dev/null +++ b/src/views/resources/viewProvider.ts @@ -0,0 +1,350 @@ +import * as vscode from "vscode"; + +import type { Focus } from "../../models/focus.ts"; +import { Account } from "../../platforms/aws/clients/account.ts"; +import { IAM } from "../../platforms/aws/clients/iam.ts"; +import { STS } from "../../platforms/aws/clients/sts.ts"; +import ARN from "../../platforms/aws/models/arnModel.ts"; +import AWSConfig from "../../platforms/aws/models/awsConfig.ts"; +import { getRegionLongName } from "../../platforms/aws/models/regionModel.ts"; +import { ProviderFactory } from "../../platforms/aws/services/providerFactory.ts"; + +import { + ResourceArnTreeItem, + ResourceErrorTreeItem, + ResourcePlaceholderTreeItem, + ResourceProfileTreeItem, + ResourceRegionTreeItem, + ResourceServiceTypeTreeItem, +} from "./treeItems.ts"; +import type { ResourceTreeItem } from "./treeItems.ts"; + +/** The synthetic profile id used for the LocalStack emulator instance. */ +const LOCALSTACK_PROFILE_ID = "localstack"; + +/** + * Whether a profile targets a LocalStack emulator rather than real AWS. A + * profile is treated as LocalStack when it resolves to a custom `endpoint_url` + * (LocalStack profiles are configured with one) or when it is the synthetic + * `localstack` instance profile. Profiles without a custom endpoint target AWS. + */ +function isLocalStackProfile(profileId: string): boolean { + return ( + profileId === LOCALSTACK_PROFILE_ID || + AWSConfig.getEndpointForProfile(profileId) !== undefined + ); +} + +/** + * Provider for a view that shows all the profile/region/service/resource information + * that is in focus + */ +export class ResourceViewProvider + implements vscode.TreeDataProvider +{ + /** The focus that determines what is shown in this view */ + private focus?: Focus = undefined; + + /** + * Produces the active focus. Stored (rather than just the resulting focus) + * so a manual refresh can recompute it from scratch — re-querying the + * LocalStack metamodel API to pick up resources created since the last view. + */ + private focusProducer?: () => Promise; + + /** EventEmitter we use to produce the event when the tree data changes. */ + private _onDidChangeTreeData = new vscode.EventEmitter< + ResourceTreeItem | undefined | null + >(); + + /** The event that is fired when the tree data changes. For notifying listeners */ + public readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + /** + * Set the active focus from a producer. The producer is retained so a manual + * refresh can re-run it (e.g. re-fetching the metamodel for a LocalStack + * instance). A producer that resolves to `undefined` (e.g. a selection with + * no focus selectors) leaves the current focus untouched. + */ + public async setFocusProducer( + producer: () => Promise, + ): Promise { + this.focusProducer = producer; + await this.applyFocus(false); + } + + /** Set the active focus directly (no producer; refresh re-renders as-is). */ + public setFocus(focus: Focus) { + this.focus = focus; + this.focusProducer = undefined; + this._onDidChangeTreeData.fire(undefined); // refresh the whole tree + } + + /** + * Manual refresh: recompute the focus from its producer (re-querying the + * LocalStack API so newly-created resources appear), then re-render. With no + * producer, re-render the current focus, which re-lists resources live. + */ + public async refresh(): Promise { + await this.applyFocus(true); + } + + /** + * Recompute and apply the focus. `forceRender` re-fires the tree-change event + * even when the producer yields no new focus, so a manual refresh always + * re-lists resources. + */ + private async applyFocus(forceRender: boolean): Promise { + if (!this.focusProducer) { + if (forceRender) { + this._onDidChangeTreeData.fire(undefined); + } + return; + } + try { + const focus = await this.focusProducer(); + if (focus) { + this.focus = focus; + this._onDidChangeTreeData.fire(undefined); + } else if (forceRender) { + /* A forced refresh whose producer now yields no focus means the + * active focus selector no longer resolves — e.g. its saved view was + * removed (or renamed). Clear to the placeholder rather than leaving + * stale content. (A plain selection change that yields no focus, with + * forceRender false, leaves the current focus untouched.) */ + this.focus = undefined; + this._onDidChangeTreeData.fire(undefined); + } + } catch (error) { + void vscode.window.showWarningMessage( + `Could not load resources: ${String(error)}`, + ); + } + } + + public getTreeItem( + element: ResourceTreeItem, + ): vscode.TreeItem | Thenable { + return element; + } + + public getChildren( + element?: ResourceTreeItem, + ): vscode.ProviderResult { + if (!element) { + if (!this.focus) { + return Promise.resolve([ + new ResourcePlaceholderTreeItem( + "Please select a focus in the Explore view.", + ), + ]); + } else { + return this.makeResourceProfiles(this.focus); + } + } else if (element instanceof ResourceProfileTreeItem) { + return this.makeResourceRegions(element); + } else if (element instanceof ResourceRegionTreeItem) { + return this.makeResourceServiceTypes(element); + } else if (element instanceof ResourceServiceTypeTreeItem) { + return this.makeResourceArns(element); + } + return Promise.resolve([]); + } + + public getParent?(element: ResourceTreeItem) { + return null; + } + + public resolveTreeItem?( + item: vscode.TreeItem, + element: ResourceTreeItem, + token: vscode.CancellationToken, + ): vscode.ProviderResult { + throw new Error("Method not implemented."); + } + + /** + * Create ResourceProfileTreeItems from the profiles in the focus. The profiles must have + * valid names, and can not be a wildcard. We need to fetch the profile account's number and name. + */ + private makeResourceProfiles( + focus: Focus, + ): vscode.ProviderResult { + let profiles = focus.profiles; + + /* if there's a wildcard profile, then fetch all profiles from AWSConfig */ + if (profiles.length === 1 && profiles[0].id === "*") { + const regions = profiles[0].regions; + profiles = AWSConfig.getProfileNames().map((profileId) => { + return { id: profileId, regions }; + }); + } + + /* else, show only the profiles specified in the focus */ + return Promise.all( + profiles.map(async (profile) => { + return Promise.all([ + STS.getCallerIdentity(profile.id), + IAM.getAccountAlias(profile.id), + ]) + .then(([{ account }, alias]) => { + return new ResourceProfileTreeItem( + profile, + account, + alias, + isLocalStackProfile(profile.id), + ); + }) + .catch((error) => { + /* error communicating with AWS, possibly bad credentials */ + return new ResourceErrorTreeItem( + `Invalid Profile: ${profile.id}. ${error}`, + ); + }); + }), + ); + } + + /** + * Create ResourceRegionTreeItems from the regions in the profile. + */ + private makeResourceRegions( + parent: ResourceProfileTreeItem, + ): vscode.ProviderResult { + /* + * If there's a single region listed, and the region name is "*", then dynamically list all of + * the actual regions available in the current profile. + */ + const regions = parent.profile.regions; + if (regions.length === 1 && regions[0].id === "*") { + const services = regions[0].services; + return Account.listRegions(parent.profile.id).then((regions) => { + return regions.map((region) => { + const longName = getRegionLongName(region); + const regionFocus = { id: region, services }; + return new ResourceRegionTreeItem(parent, regionFocus, longName); + }); + }); + } + + /* If the region name is 'default', then only show the user's currently selected default region */ + if (regions.length === 1 && regions[0].id === "default") { + const region = AWSConfig.getRegionForProfile(parent.profile.id); + if (!region) { + return Promise.resolve([ + new ResourceErrorTreeItem( + `Profile ${parent.profile.id} does not have a default region configured.`, + ), + ]); + } + const regionFocus = { id: region, services: regions[0].services }; + return Promise.resolve([ + new ResourceRegionTreeItem( + parent, + regionFocus, + getRegionLongName(region), + ), + ]); + } + + /* else, show only the specified regions */ + return regions.map( + (region) => + new ResourceRegionTreeItem( + parent, + region, + getRegionLongName(region.id), + ), + ); + } + + /** + * Create the combined service/resource-type rows for a region — one row per + * (service, resource type) pair. If the region's service list is a wildcard, + * expand it to all supported providers (each with all of its resource types). + */ + private makeResourceServiceTypes( + parent: ResourceRegionTreeItem, + ): vscode.ProviderResult { + const services = parent.region.services; + const expanded = + services.length === 1 && services[0].id === "*" + ? ProviderFactory.getSupportedServices().map((provider) => ({ + provider, + service: { + id: provider.getId(), + resourcetypes: provider + .getResourceTypes() + .map((name) => ({ id: name, arns: ["*"] })), + }, + })) + : services.map((service) => ({ + provider: ProviderFactory.getProviderForService(service.id), + service, + })); + + const rows: ResourceTreeItem[] = []; + for (const { provider, service } of expanded) { + for (const resourceType of service.resourcetypes) { + rows.push( + new ResourceServiceTypeTreeItem( + parent, + service, + provider, + resourceType, + ), + ); + } + } + return rows; + } + + /** + * Create ResourceArnTreeItems from the ARNs in the combined service/type row. + */ + private makeResourceArns( + parent: ResourceServiceTypeTreeItem, + ): vscode.ProviderResult { + const profile = parent.parent.parent.profile.id; + const region = parent.parent.region.id; + const serviceProvider = parent.provider; + const serviceName = serviceProvider.getName(); + const [singularName, _] = serviceProvider.getResourceTypeNames( + parent.resourceType.id, + ); + + /* + * Cases: + * 1) Wildcard ARN: ["*"] - fetch all ARNs of this resource type from AWS + * 2) Specific ARNs: ["arn:aws:..."] - use the specified ARNs directly + * 3) No ARNs: [] - display a placeholder tree item + */ + const arnSpecs = parent.resourceType.arns; + const useWildCard = arnSpecs.length === 1 && arnSpecs[0] === "*"; + const arnPromise = useWildCard + ? serviceProvider.getResourceArns(profile, region, parent.resourceType.id) + : Promise.resolve(arnSpecs); + + return arnPromise.then((arns) => { + if (arns.length === 0) { + return [new ResourcePlaceholderTreeItem()]; + } else { + return arns.map((arn) => { + /* Most providers return ARNs, but some return a primary identifier + * that isn't a parseable ARN. Fall back to showing the identifier + * verbatim rather than failing the whole row. */ + let name: string; + try { + name = new ARN(arn).resourceName || arn; + } catch { + name = arn; + } + + /* Tooltip has form: */ + const tooltip = `${serviceName} ${singularName}`; + return new ResourceArnTreeItem(parent, arn, name, tooltip); + }); + } + }); + } +} diff --git a/tsconfig.json b/tsconfig.json index f02509c..74e3182 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "target": "esnext", "lib": ["ESNext", "DOM"], "moduleResolution": "bundler", + "resolveJsonModule": true, "sourceMap": true, "strict": true /* enable all strict type-checking options */, "allowImportingTsExtensions": true,