Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
27cddff
docs(openspec): add integrate-resource-browsers change proposal
peter-smith-phd Jun 21, 2026
8922caa
Initial commit of Claude-generated changes.
peter-smith-phd Jun 21, 2026
89cd8c5
feat(explore): row action icons, adaptive settings target, eslint typ…
peter-smith-phd Jun 22, 2026
7b865cc
feat(explore): seventh-round UX refinements
peter-smith-phd Jun 22, 2026
e80b4a3
feat(explore): resource browser fixes + display rework
peter-smith-phd Jun 22, 2026
8159a64
fix(explore): only list successfully-created CloudFormation stacks
peter-smith-phd Jun 22, 2026
1b0e218
test(explore): add awsConfig + cfnStackModel model tests; archive change
peter-smith-phd Jun 22, 2026
5f118fc
chore(explore): record completed tasks in archived tasks.md
peter-smith-phd Jun 22, 2026
3a10a1c
feat(resources): support all LocalStack services via manifest + decla…
peter-smith-phd Jun 23, 2026
0a05c33
feat(resources): replace AWS service icons with target-aware icons
peter-smith-phd Jun 23, 2026
4af3e1c
Checkpoint commit
peter-smith-phd Jun 23, 2026
3868e48
Fix biome errors/warnings
peter-smith-phd Jun 23, 2026
2512bc6
Merge OpenSpec changes into a single archived change.
peter-smith-phd Jun 23, 2026
cf097f0
Fix test failures.
peter-smith-phd Jun 23, 2026
38a6d17
refactor(explore): rename filter→view, remove dead StandardModel code
peter-smith-phd Jun 24, 2026
6d7207a
refactor(providers): convert remaining AWS providers to declarative d…
peter-smith-phd Jun 24, 2026
8e4f903
fix(regions): don't crash the Resources view on an un-tabled region
peter-smith-phd Jun 24, 2026
957d296
fix(resources): resolve CloudFormation-origin API Gateway & Cognito r…
peter-smith-phd Jun 24, 2026
5df3742
fix(resources): dedupe and invalidate memoized AWS caches
peter-smith-phd Jun 24, 2026
28c4ad9
feat(resources): move Resource Details to the bottom panel
peter-smith-phd Jun 24, 2026
2405a74
chore(deps): unpin AWS SDK, satisfy trust policies, fix vsce packaging
peter-smith-phd Jun 24, 2026
f9477f6
Fix biome linter error.
peter-smith-phd Jun 24, 2026
e058fd5
Revert README changes, with the intention of doing this properly befo…
peter-smith-phd Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build/extension.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
167 changes: 167 additions & 0 deletions build/generate-detail-fields.mjs
Original file line number Diff line number Diff line change
@@ -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/<service>-*.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 <model.normal.json> <OperationName>
*
* 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 <model.normal.json> <OperationName>",
);
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();
}
116 changes: 116 additions & 0 deletions build/generate-service-manifest.mjs
Original file line number Diff line number Diff line change
@@ -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();
5 changes: 5 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-21
Loading