Files
pbakaus_impeccable/scripts/lib/vscode-extension.js
T
Paul BakausandGitHub 4957fd7069 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.
2026-09-07 11:40:31 -07:00

66 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}