Add declarative VS Code skill extension packaging (#775)

* Add declarative VS Code skill extension packaging

Stage a launcher-only Copilot skill bundle, validate relocation and VSIX packaging, and keep project-install hooks out of the extension.

AI assistance: Codex, under maintainer direction.

* Document verified Copilot extension smoke test

Record the successful read-only VS Code 1.136.1 run and the reload required after initial workspace trust. Keep minimum-version and remote smoke gaps explicit.

AI assistance: Codex, under maintainer direction.

* Fix VS Code packaging test version source

Derive the provider fixture version from the skill manifest and assert the packaged skill and extension versions agree.

AI assistance: Codex, under maintainer direction.
This commit is contained in:
Paul Bakaus
2026-09-07 11:40:31 -07:00
committed by GitHub
parent 93bfe70c58
commit 4957fd7069
9 changed files with 250 additions and 0 deletions
+5
View File
@@ -33,6 +33,7 @@ import {
verifyPluginAgentRewrite,
} from './lib/plugin-paths.js';
import { stageOpenAIPlugin } from './lib/openai-plugin.js';
import { stageVSCodeExtension } from './lib/vscode-extension.js';
import { ENGINE_TARGETS, binaryName, main as fetchEngineMain, readEngineVersion } from './fetch-engine.mjs';
// Sub-page generation is now handled by Astro content collections.
@@ -895,6 +896,10 @@ async function build() {
const openAiPluginRoot = stageOpenAIPlugin(ROOT_DIR, DIST_DIR);
await createProviderZip(openAiPluginRoot, DIST_DIR, 'openai-plugin');
// Declarative Marketplace skill bundle: no editor runtime or project hooks.
// Staged before optional engine bundling to keep the VSIX launcher-only.
stageVSCodeExtension(ROOT_DIR, DIST_DIR);
// Release zips ship launcher-only by default: the launcher downloads the
// pinned engine on first run. IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging
// every fetched target into every dist skill copy for offline installs.
+65
View File
@@ -0,0 +1,65 @@
import fs from 'node:fs';
import path from 'node:path';
import { rewritePluginMarkdownTree } from './plugin-paths.js';
const PROJECT_SCRIPTS = '.github/skills/impeccable/scripts';
const PROJECT_FALLBACK =
`That base directory resolves every \`${PROJECT_SCRIPTS}/impeccable <verb>\` command in this skill and its references, ` +
`and \`${PROJECT_SCRIPTS}\` is the fallback only when the runtime reports no base directory.`;
// VS Code provides the loaded skill's URI, not Claude's substitution variables.
// Relative Markdown links resolve beside that file; shell commands still run
// from the user's project. Never fall back to another installed skill copy.
export function rewriteVSCodeMarkdown(content, { isSkillEntrypoint = false } = {}) {
if (isSkillEntrypoint) {
if (!content.includes(PROJECT_FALLBACK)) {
throw new Error('VS Code skill path rewrite drift: Setup fallback changed.');
}
content = content.replace(PROJECT_FALLBACK,
'Resolve the installed directory from this skills [launcher](scripts/impeccable). ' +
'Replace `<skill-base-dir>` with that absolute directory before running commands; it is not a shell variable.');
}
return content.replaceAll(PROJECT_SCRIPTS, '<skill-base-dir>/scripts')
.replace(/(?<!["\w/])<skill-base-dir>\/scripts\/impeccable(\.cmd)?(?=[\s`])/g,
'"<skill-base-dir>/scripts/impeccable$1"');
}
/** Stage a declarative skill extension, independently of repo-install sidecars. */
export function stageVSCodeExtension(rootDir, distDir) {
const source = path.join(distDir, 'github', '.github', 'skills', 'impeccable');
const version = JSON.parse(fs.readFileSync(path.join(rootDir, '.claude-plugin/plugin.json'), 'utf8')).version;
const extensionRoot = path.join(distDir, 'vscode');
// Only this generated artifact is replaced. Never touch the user's editor.
fs.rmSync(extensionRoot, { recursive: true, force: true });
fs.mkdirSync(extensionRoot, { recursive: true });
const skillDir = path.join(extensionRoot, 'skills', 'impeccable');
fs.cpSync(source, skillDir, { recursive: true });
rewritePluginMarkdownTree(skillDir, rewriteVSCodeMarkdown);
const manifest = {
name: 'impeccable',
displayName: 'Impeccable',
description: 'Design skills for GitHub Copilot: build, critique, audit, and refine interfaces.',
version,
publisher: 'pbakaus',
license: 'Apache-2.0',
homepage: 'https://impeccable.style',
repository: { type: 'git', url: 'https://github.com/pbakaus/impeccable.git' },
bugs: { url: 'https://github.com/pbakaus/impeccable/issues' },
icon: 'icon.png',
categories: ['AI'],
keywords: ['copilot', 'design', 'skills', 'frontend', 'accessibility'],
engines: { vscode: '^1.109.3' },
extensionKind: ['workspace'],
extensionDependencies: ['GitHub.copilot-chat'],
capabilities: { untrustedWorkspaces: { supported: false }, virtualWorkspaces: false },
contributes: { chatSkills: [{ path: './skills/impeccable/SKILL.md' }] },
};
fs.writeFileSync(path.join(extensionRoot, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`);
fs.copyFileSync(path.join(rootDir, 'scripts/lib/assets/plugin-icon.png'), path.join(extensionRoot, 'icon.png'));
fs.copyFileSync(path.join(rootDir, 'LICENSE'), path.join(extensionRoot, 'LICENSE'));
fs.copyFileSync(path.join(rootDir, 'vscode/README.md'), path.join(extensionRoot, 'README.md'));
fs.copyFileSync(path.join(rootDir, 'vscode/.vscodeignore'), path.join(extensionRoot, '.vscodeignore'));
return extensionRoot;
}
+2
View File
@@ -33,6 +33,7 @@ export const SUITES = {
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/)/,
/^ENGINE_VERSION$/,
/^README(\.npm)?\.md$/,
/^vscode\//,
/^\.github\/workflows\/release-engine\.yml$/,
/^cli\/bin\//,
],
@@ -72,6 +73,7 @@ export const SUITES = {
'tests/github-sheriff.test.mjs',
'tests/hook-build.test.mjs',
'tests/openai-plugin.test.mjs',
'tests/vscode-extension.test.mjs',
'tests/process-group.test.mjs',
'tests/release.test.mjs',
'tests/bundle-signing.test.mjs',