Files
awesome-copilot/extensions/pr-artifact-explorer/extension.mjs
T
github-actions[bot] e321e4a9c7 chore: publish from main
2026-08-06 12:30:14 +00:00

216 lines
7.3 KiB
JavaScript

import {
CanvasError,
createCanvas,
joinSession,
} from "@github/copilot-sdk/extension";
import { resolveAccounts, invalidateAccounts } from "./accounts.mjs";
import {
clearArtifactCache,
deleteCachedArtifact,
getCacheSummary,
inspectArtifact,
} from "./cache.mjs";
import {
broadcastCache,
closeAllPreviews,
navigateInstance,
startInstance,
stopInstance,
} from "./server.mjs";
import { stopStaticPreviewsForArtifact } from "./preview.mjs";
import { loadPrefs, normalizeRepository, savePrefs } from "./state.mjs";
function positiveInteger(value, label) {
const number = Number.parseInt(value, 10);
if (!Number.isSafeInteger(number) || number <= 0) {
throw new CanvasError("invalid_input", `${label} must be a positive integer.`);
}
return number;
}
const session = await joinSession({
canvases: [
createCanvas({
id: "pr-artifact-explorer",
displayName: "Artifact Explorer",
description:
"Browse GitHub pull requests and securely preview workflow artifacts, including static sites and asciinema recordings.",
inputSchema: {
type: "object",
properties: {
repository: {
type: "string",
description: "Repository in owner/name format.",
},
pullNumber: {
type: "integer",
minimum: 1,
description: "Pull request number to open.",
},
url: {
type: "string",
description: "GitHub pull request URL to open.",
},
},
additionalProperties: false,
},
actions: [
{
name: "open_pull_request",
description: "Navigate an open explorer canvas to a pull request.",
inputSchema: {
type: "object",
properties: {
repository: { type: "string" },
pullNumber: { type: "integer", minimum: 1 },
},
required: ["repository", "pullNumber"],
additionalProperties: false,
},
handler: (ctx) => {
const repository = normalizeRepository(ctx.input?.repository);
const pullNumber = positiveInteger(ctx.input?.pullNumber, "pullNumber");
const route = `#/pull/${encodeURIComponent(repository)}/${pullNumber}`;
return {
navigated: navigateInstance(ctx.instanceId, route),
repository,
pullNumber,
};
},
},
{
name: "inspect_artifact",
description: "Download, index, and smart-detect a GitHub Actions artifact.",
inputSchema: {
type: "object",
properties: {
repository: { type: "string" },
artifactId: { type: "integer", minimum: 1 },
},
required: ["repository", "artifactId"],
additionalProperties: false,
},
handler: async (ctx) => {
const repository = normalizeRepository(ctx.input?.repository);
const artifactId = positiveInteger(ctx.input?.artifactId, "artifactId");
const prefs = await loadPrefs();
const auth = await resolveAccounts({
preferredId: prefs.account,
repository,
});
if (!auth.activeToken) {
throw new CanvasError("github_auth_required", "No usable GitHub account was found.");
}
const metadata = await inspectArtifact(auth.activeToken, repository, artifactId);
await broadcastCache();
navigateInstance(
ctx.instanceId,
`#/artifact/${encodeURIComponent(repository)}/0/${artifactId}`,
);
const entries = metadata.analysis.entries ?? [];
return {
artifact: metadata.artifact,
analysis: {
...metadata.analysis,
entries: entries.slice(0, 200),
entriesTruncated: entries.length > 200,
},
};
},
},
{
name: "cache_status",
description: "Report downloaded artifact cache usage.",
handler: async () => getCacheSummary(),
},
{
name: "clear_cache",
description: "Delete one downloaded artifact or clear the entire artifact cache.",
inputSchema: {
type: "object",
properties: {
artifactId: { type: "integer", minimum: 1 },
},
additionalProperties: false,
},
handler: async (ctx) => {
if (ctx.input?.artifactId != null) {
await stopStaticPreviewsForArtifact(ctx.input.artifactId);
const result = await deleteCachedArtifact(
positiveInteger(ctx.input.artifactId, "artifactId"),
);
navigateInstance(ctx.instanceId, "#/cache");
await broadcastCache({ refresh: true });
return result;
}
await closeAllPreviews();
const result = await clearArtifactCache();
navigateInstance(ctx.instanceId, "#/cache");
await broadcastCache({ refresh: true });
return result;
},
},
{
name: "accounts",
description: "List GitHub accounts inherited from the Copilot app and GitHub CLI.",
handler: async () => {
const prefs = await loadPrefs();
const auth = await resolveAccounts({
preferredId: prefs.account,
repository: prefs.repository,
force: true,
});
return { active: auth.active, accounts: auth.accounts };
},
},
{
name: "set_account",
description: "Select the GitHub account used by the artifact explorer.",
inputSchema: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
additionalProperties: false,
},
handler: async (ctx) => {
if (typeof ctx.input?.id !== "string" || !ctx.input.id) {
throw new CanvasError("invalid_account", "Account id is required.");
}
const prefs = await loadPrefs();
invalidateAccounts();
const auth = await resolveAccounts({
preferredId: ctx.input.id,
repository: prefs.repository,
force: true,
});
if (!auth.active || auth.active.id !== ctx.input.id) {
throw new CanvasError(
"invalid_account",
`Account "${ctx.input.id}" was not found or is unavailable.`,
);
}
prefs.account = ctx.input.id;
await savePrefs(prefs);
return { active: auth.active, accounts: auth.accounts };
},
},
],
open: async (ctx) => {
const entry = await startInstance(
ctx.instanceId,
ctx.input,
(message) => session.log(message, { level: "debug" }),
);
return {
title: "Artifact Explorer",
status: "GitHub pull request artifacts",
url: entry.url,
};
},
onClose: async (ctx) => {
await stopInstance(ctx.instanceId);
},
}),
],
});