From eaf51f1763451def62eab6f89c9726cc186f7a42 Mon Sep 17 00:00:00 2001 From: eFAILution Date: Fri, 2 Oct 2026 09:25:37 -0400 Subject: [PATCH 1/2] chore(demo): add the demo GIF recorder Scripts under scripts/demo-gifs drive a packaged VSIX in a throwaway VS Code profile over CDP and encode each scene to a GIF, plus a per-second review sheet for the privacy check. They have their own package.json and are already excluded from the VSIX by scripts/** in .vscodeignore. Documents the workflow, layout, decision, and failure modes in .ai/. --- .ai/architecture.yaml | 18 + .ai/context.yaml | 1 + .ai/decisions.yaml | 32 + .ai/errors.yaml | 19 + .ai/index.yaml | 4 + .ai/workflows.yaml | 32 + eslint.config.js | 11 + scripts/demo-gifs/fixtures/settings.json | 59 + .../demo-gifs/fixtures/templates/deploy.yml | 16 + scripts/demo-gifs/lib/config.mjs | 25 + scripts/demo-gifs/lib/input.mjs | 85 + scripts/demo-gifs/lib/recorder.mjs | 71 + scripts/demo-gifs/lib/vscode.mjs | 118 ++ scripts/demo-gifs/lib/workspace.mjs | 31 + scripts/demo-gifs/package-lock.json | 1705 +++++++++++++++++ scripts/demo-gifs/package.json | 13 + scripts/demo-gifs/record.mjs | 61 + scripts/demo-gifs/scenes/browse.mjs | 42 + scripts/demo-gifs/scenes/complete.mjs | 45 + scripts/demo-gifs/scenes/hover.mjs | 56 + scripts/demo-gifs/scenes/validate.mjs | 77 + scripts/demo-gifs/scenes/versions.mjs | 43 + 22 files changed, 2564 insertions(+) create mode 100644 scripts/demo-gifs/fixtures/settings.json create mode 100644 scripts/demo-gifs/fixtures/templates/deploy.yml create mode 100644 scripts/demo-gifs/lib/config.mjs create mode 100644 scripts/demo-gifs/lib/input.mjs create mode 100644 scripts/demo-gifs/lib/recorder.mjs create mode 100644 scripts/demo-gifs/lib/vscode.mjs create mode 100644 scripts/demo-gifs/lib/workspace.mjs create mode 100644 scripts/demo-gifs/package-lock.json create mode 100644 scripts/demo-gifs/package.json create mode 100644 scripts/demo-gifs/record.mjs create mode 100644 scripts/demo-gifs/scenes/browse.mjs create mode 100644 scripts/demo-gifs/scenes/complete.mjs create mode 100644 scripts/demo-gifs/scenes/hover.mjs create mode 100644 scripts/demo-gifs/scenes/validate.mjs create mode 100644 scripts/demo-gifs/scenes/versions.mjs diff --git a/.ai/architecture.yaml b/.ai/architecture.yaml index 21773d61..3c36da84 100644 --- a/.ai/architecture.yaml +++ b/.ai/architecture.yaml @@ -143,6 +143,24 @@ components: publish-release.sh: Publish to VS Code marketplace release-and-publish.sh: Combined release and publish setup-github-token.sh: GitHub token configuration + demo_gifs: + location: scripts/demo-gifs/ + purpose: Maintainer tool that records the README demo GIFs by driving a real VS Code through Playwright over CDP + packaged: false (scripts/** in .vscodeignore; own package.json, out/ and node_modules/ gitignored) + files: + record.mjs: Entry point. Resets the work dir, packages + installs the VSIX, warms the cache, records scenes + lib/config.mjs: Paths, viewport (1280x800 at 2x), GIF size (1100 wide, 12 fps), env overrides + lib/workspace.mjs: Throwaway profile, demo workspace (my-pipelines), vsce package + install + lib/vscode.mjs: Launch/stop VS Code, Command Palette, scene reset, webview frame lookup + lib/input.mjs: Drawn cursor, eased mouse moves, typing, wheel scrolling + lib/recorder.mjs: CDP screencast capture, ffmpeg GIF encode, per-second review sheet + scenes/: One module per GIF (browse, complete, hover, validate, versions) + fixtures/: Profile settings.json (component sources, quiet editor) and templates/deploy.yml for local includes + how_it_works: + - VS Code starts with --remote-debugging-port; Playwright attaches with chromium.connectOverCDP + - Emulation.setDeviceMetricsOverride fixes the viewport so output size doesn't depend on the screen + - The OS pointer isn't captured, so a cursor element is drawn in the top document and moved with page.mouse + - Page.startScreencast frames only arrive on change; an ffmpeg concat list holds each frame until the next interfaces: Component: location: src/types/git-component.ts diff --git a/.ai/context.yaml b/.ai/context.yaml index a12fc9a5..bce25a4d 100644 --- a/.ai/context.yaml +++ b/.ai/context.yaml @@ -29,6 +29,7 @@ structure: types: src/types/ utils: src/utils/ scripts: scripts/ + demo_gifs: scripts/demo-gifs/ context_modules: architecture: architecture.yaml workflows: workflows.yaml diff --git a/.ai/decisions.yaml b/.ai/decisions.yaml index 4a5b3a53..352d0960 100644 --- a/.ai/decisions.yaml +++ b/.ai/decisions.yaml @@ -427,6 +427,38 @@ decisions: - .mocharc.cjs - package.json (test scripts) title: Mocha Over Jest + DEMO_GIFS_RECORDED_BY_SCRIPT: + date: '2026-10' + status: accepted + context: README/Marketplace GIFs were hand-recorded, went stale as the UI changed, and risked showing the + maintainer's own projects and paths + decision: Record them with scripts/demo-gifs (Playwright driving a packaged VSIX in a throwaway profile) and + host the output as GitHub user-attachments linked from README.md + rationale: + - Re-recording after a UI change is one command, and every take looks the same + - Fresh profile, neutral my-pipelines folder and public gitlab.com components keep personal details out + - Installing the packaged VSIX keeps "[Extension Development Host]" out of the title bar + - user-attachments URLs are public for a public repo and render on the Marketplace; committing GIFs would + grow the repo by ~4 MB per re-record + consequences: + positive: + - Demos can be refreshed with every UI change + - Review sheets make the privacy check quick + negative: + - macOS + local VS Code only; not run in CI + - user-attachments links stop rendering publicly if the repo goes private, and GitHub doesn't promise + they last forever + alternatives_considered: + - name: manual screen recording + rejected_because: Inconsistent, slow to redo, easy to leak personal details + - name: commit GIFs under images/ + rejected_because: Repo growth on every re-record + - name: extensionDevelopmentPath instead of a VSIX + rejected_because: Title bar shows [Extension Development Host] + references: + - scripts/demo-gifs/record.mjs + - .ai/workflows.yaml (record_demo_gifs) + title: Demo GIFs Recorded By Script future_decisions: - question: Should we support other CI/CD platforms (GitHub Actions, CircleCI)? status: under_consideration diff --git a/.ai/errors.yaml b/.ai/errors.yaml index 3d6a7a33..8319b5b3 100644 --- a/.ai/errors.yaml +++ b/.ai/errors.yaml @@ -391,6 +391,25 @@ errors: - Re-enable v10 once @release-it/conventional-changelog adopts the render-function stack (conventional-changelog@8 / writer@9) reference: .github/dependabot.yml + package.json; see huntridge-labs/argus#318 + demo_gif_recording_fails: + symptom: npm run record in scripts/demo-gifs fails or produces a wrong-looking GIF + causes: + - cause: 'listen EINVAL on .../1.13-main.sock or "IPC handle is longer than 103 chars"' + solution: Point GCH_DEMO_DIR at a short path (default /tmp/gch-demo) + - cause: 'mach_port_rendezvous ... Permission denied, or "Starting inspector failed: operation not permitted"' + solution: Run outside the command sandbox; VS Code can't start inside it + - cause: VS Code never opened its debug port, or a stray instance holds the profile + solution: pkill -9 -f "user-data-dir=/tmp/gch-demo/profile" and run again + - cause: Command Palette did not highlight a command + solution: The command title changed in VS Code; update the name in the scene or lib/vscode.mjs + - cause: An empty "Drag a view here" pane or a side bar shows up + solution: prepareScene hides side bars by checking visibility; check the .part.* selectors still match + - cause: Typing lands at the wrong indent or Enter accepts a suggestion + solution: Scenes rely on fixtures/settings.json (wordBasedSuggestions off) and VS Code's YAML auto-indent; + re-check those after a VS Code update + - cause: A webview element is never found + solution: Webviews are out-of-process frames that only attach while the script is connected; open the + panel inside the same run and use webviewFrame(page, selector) debugging: enable_debug_logs: steps: diff --git a/.ai/index.yaml b/.ai/index.yaml index e06db923..af3737f5 100644 --- a/.ai/index.yaml +++ b/.ai/index.yaml @@ -19,6 +19,7 @@ keys: - cache - component - constants + - demo_gifs - errors - extension - parsers @@ -35,6 +36,7 @@ keys: - build_and_package - debug_extension - fix_failing_test + - record_demo_gifs - release - run_tests - setup_dev_environment @@ -43,6 +45,7 @@ keys: - BATCH_API_REQUESTS - CACHE_COMPONENTS - CENTRALIZED_ERROR_HANDLING + - DEMO_GIFS_RECORDED_BY_SCRIPT - DEPENDABOT_TARGETS_BETA - EXTENSION_HOST_TEST_LAYER - FILE_SIZE_POLICY @@ -62,6 +65,7 @@ keys: - cache_issues - compilation_errors - component_not_loading + - demo_gif_recording_fails - empty_changelog_conventionalcommits_v10 - extension_not_activating - hover_not_working diff --git a/.ai/workflows.yaml b/.ai/workflows.yaml index 09699ef5..af7b5103 100644 --- a/.ai/workflows.yaml +++ b/.ai/workflows.yaml @@ -138,6 +138,38 @@ workflows: command: npm test - action: commit_changes command: 'git commit -m ''chore: update dependencies''' + record_demo_gifs: + description: Re-record the README demo GIFs (browse, complete, hover, validate, versions) after a UI change + location: scripts/demo-gifs/ + prerequisites: + - macOS with VS Code installed at /Applications (override with VSCODE_BIN / VSCODE_CLI) + - ffmpeg on PATH + - Network access to gitlab.com (scenes use public components/opentofu, sast, secret-detection, code-quality) + - Run outside any command sandbox; VS Code needs its mach port and debug port + steps: + - action: install_recorder_dependencies + command: cd scripts/demo-gifs && npm install + note: Own package.json/lock, separate from the extension's; root npm install never touches it + - action: record + command: npm run record + note: 'All scenes, or name some: npm run record -- hover versions. Packages the extension with vsce, + installs it into a throwaway profile under /tmp/gch-demo (GCH_DEMO_DIR), drives VS Code over CDP, + writes out/.gif and out/-review.png' + - action: privacy_review + check: Open every out/-review.png (one tile per second) and confirm no names, paths, accounts, + private projects or tokens appear. The profile is fresh and the folder is my-pipelines, but check anyway + - action: publish + steps: + - Drag the GIFs into a comment on the README PR on GitHub and post it + - Copy the github.com/user-attachments/assets/ URLs from the comment into README.md + - Confirm each URL loads without logging in (curl -sL -o /dev/null -w '%{http_code}' returns 200) + note: GIFs are not committed; see decisions.yaml DEMO_GIFS_RECORDED_BY_SCRIPT + - action: add_or_change_a_scene + location: scripts/demo-gifs/scenes/{name}.mjs + contract: Export cursorStart, prepare(page) and perform(page); register it in SCENES in record.mjs + helpers: lib/vscode.mjs (prepareScene, runCommand, webviewFrame), lib/input.mjs (moveTo, clickOn, typeSlow, + wordPosition, scrollIntoView) + reference: scripts/demo-gifs/record.mjs common_commands: compile: npm run compile watch: npm run watch diff --git a/eslint.config.js b/eslint.config.js index 964ec2b5..f5cf9078 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -40,6 +40,17 @@ module.exports = [ ...js.configs.recommended.rules, }, }, + { + // Demo GIF recorder, a maintainer tool. Its page.evaluate callbacks run in the workbench, hence browser globals. + files: ['scripts/demo-gifs/**/*.mjs'], + languageOptions: { + sourceType: 'module', + globals: { ...globals.node, ...globals.browser }, + }, + rules: { + ...js.configs.recommended.rules, + }, + }, { // Webview client scripts run in the Electron renderer, not the extension host files: ['src/webview/client/**/*.ts'], diff --git a/scripts/demo-gifs/fixtures/settings.json b/scripts/demo-gifs/fixtures/settings.json new file mode 100644 index 00000000..367dbe2d --- /dev/null +++ b/scripts/demo-gifs/fixtures/settings.json @@ -0,0 +1,59 @@ +{ + "workbench.startupEditor": "none", + "workbench.tips.enabled": false, + "workbench.colorTheme": "Dark Modern", + "editor.minimap.enabled": false, + "editor.fontSize": 15, + "editor.lineHeight": 24, + "editor.hover.delay": 300, + "editor.stickyScroll.enabled": false, + "editor.renderWhitespace": "none", + "telemetry.telemetryLevel": "off", + "update.mode": "none", + "extensions.autoCheckUpdates": false, + "extensions.autoUpdate": "off", + "extensions.ignoreRecommendations": true, + "workbench.enableExperiments": false, + "chat.disableAIFeatures": true, + "security.workspace.trust.enabled": false, + "git.enabled": false, + "breadcrumbs.enabled": false, + "window.commandCenter": false, + "workbench.layoutControl.enabled": false, + "window.restoreWindows": "none", + "files.autoSave": "off", + "workbench.secondarySideBar.defaultVisibility": "hidden", + "gitlabComponentHelper.logLevel": "ERROR", + "gitlabComponentHelper.componentSources": [ + { + "name": "OpenTofu", + "path": "components/opentofu", + "gitlabInstance": "gitlab.com" + }, + { + "name": "SAST", + "path": "components/sast", + "gitlabInstance": "gitlab.com" + }, + { + "name": "Secret Detection", + "path": "components/secret-detection", + "gitlabInstance": "gitlab.com" + }, + { + "name": "Code Quality", + "path": "components/code-quality", + "gitlabInstance": "gitlab.com" + } + ], + "workbench.statusBar.visible": true, + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": true + }, + "redhat.telemetry.enabled": false, + "yaml.schemaStore.enable": false, + "window.dialogStyle": "custom", + "editor.wordBasedSuggestions": "off" +} diff --git a/scripts/demo-gifs/fixtures/templates/deploy.yml b/scripts/demo-gifs/fixtures/templates/deploy.yml new file mode 100644 index 00000000..9a0648c5 --- /dev/null +++ b/scripts/demo-gifs/fixtures/templates/deploy.yml @@ -0,0 +1,16 @@ +spec: + inputs: + environment: + description: Where to deploy. + options: [staging, production] + app_name: + description: Name of the app to deploy. + replicas: + type: number + default: 2 + description: How many pods to run. +--- +deploy-$[[ inputs.environment ]]: + stage: deploy + script: + - ./deploy.sh "$[[ inputs.app_name ]]" "$[[ inputs.environment ]]" "$[[ inputs.replicas ]]" diff --git a/scripts/demo-gifs/lib/config.mjs b/scripts/demo-gifs/lib/config.mjs new file mode 100644 index 00000000..afdeb37f --- /dev/null +++ b/scripts/demo-gifs/lib/config.mjs @@ -0,0 +1,25 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const DEMO_DIR = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +export const REPO_ROOT = path.resolve(DEMO_DIR, '../..'); +export const FIXTURES_DIR = path.join(DEMO_DIR, 'fixtures'); +export const OUT_DIR = path.join(DEMO_DIR, 'out'); + +// VS Code's IPC socket lives in the profile and its path must stay under ~103 characters, so keep this short. +export const WORK_DIR = process.env.GCH_DEMO_DIR ?? '/tmp/gch-demo'; +export const PROFILE_DIR = path.join(WORK_DIR, 'profile'); +export const EXTENSIONS_DIR = path.join(WORK_DIR, 'extensions'); +// The folder name shows in the window title, so it doubles as the demo's project name. +export const WORKSPACE_DIR = path.join(WORK_DIR, 'my-pipelines'); +export const FRAMES_DIR = path.join(WORK_DIR, 'frames'); +export const VSIX_PATH = path.join(WORK_DIR, 'extension.vsix'); + +const VSCODE_APP = '/Applications/Visual Studio Code.app/Contents'; +export const VSCODE_BIN = process.env.VSCODE_BIN ?? `${VSCODE_APP}/MacOS/Code`; +export const VSCODE_CLI = process.env.VSCODE_CLI ?? `${VSCODE_APP}/Resources/app/bin/code`; +export const DEBUG_PORT = Number(process.env.GCH_DEMO_PORT ?? 9333); + +export const VIEWPORT = { width: 1280, height: 800 }; +export const DEVICE_SCALE = 2; +export const GIF = { width: 1100, fps: 12, colors: 192 }; diff --git a/scripts/demo-gifs/lib/input.mjs b/scripts/demo-gifs/lib/input.mjs new file mode 100644 index 00000000..feea836e --- /dev/null +++ b/scripts/demo-gifs/lib/input.mjs @@ -0,0 +1,85 @@ +// Mouse, keyboard, and a drawn cursor. The screencast doesn't capture the OS pointer, so the scripts draw their own +// in the top document (above the webview iframes) and move it in step with Playwright's synthetic mouse. +import { VIEWPORT } from './config.mjs'; +import { sleep } from './vscode.mjs'; + +const CURSOR_ID = 'demo-cursor'; +const CURSOR_SVG = ""; +const FRAME_MS = 16; +const TOP_SAFE_PX = 90; + +let cursor = { x: VIEWPORT.width / 2, y: VIEWPORT.height / 2 }; + +export async function showCursor(page, x, y) { + await page.evaluate(({ id, svg, x, y }) => { + document.getElementById(id)?.remove(); + const el = document.createElement('div'); + el.id = id; + el.style.cssText = `position:fixed;left:0;top:0;width:22px;height:28px;z-index:2147483647;pointer-events:none;transform:translate(${x}px,${y}px);background:url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}") no-repeat`; + document.body.appendChild(el); + }, { id: CURSOR_ID, svg: CURSOR_SVG, x, y }); + cursor = { x, y }; +} + +const easeInOut = (t) => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2); + +export async function moveTo(page, x, y, ms = 500) { + const steps = Math.max(8, Math.round(ms / FRAME_MS)); + const from = cursor; + for (let i = 1; i <= steps; i++) { + const e = easeInOut(i / steps); + const point = { x: from.x + (x - from.x) * e, y: from.y + (y - from.y) * e }; + await page.evaluate(({ id, point }) => { + const el = document.getElementById(id); + if (el) el.style.transform = `translate(${point.x}px,${point.y}px)`; + }, { id: CURSOR_ID, point }); + await page.mouse.move(point.x, point.y); + await sleep(ms / steps); + } + cursor = { x, y }; +} + +export async function centerOf(locator) { + const box = await locator.boundingBox(); + if (!box) throw new Error('Element has no bounding box'); + return { x: box.x + box.width / 2, y: box.y + box.height / 2 }; +} + +export async function clickOn(page, locator, { ms = 600, pause = 250 } = {}) { + const { x, y } = await centerOf(locator); + await moveTo(page, x, y, ms); + await sleep(pause); + await page.mouse.click(x, y); +} + +/** Middle of `word` on the first editor line containing `lineText`. */ +export async function wordPosition(page, lineText, word) { + const line = page.locator('.view-line', { hasText: lineText }).first(); + return centerOf(line.locator('span span', { hasText: word }).first()); +} + +/** Type with a little jitter so it reads as a person typing. */ +export async function typeSlow(page, text, delay = 70) { + for (const ch of text) { + await (ch === '\n' ? page.keyboard.press('Enter') : page.keyboard.type(ch)); + await sleep(delay + Math.random() * 30); + } +} + +export async function wheel(page, deltaY, { steps = 10, ms = 600 } = {}) { + for (let i = 0; i < steps; i++) { + await page.mouse.wheel(0, deltaY / steps); + await sleep(ms / steps); + } +} + +/** Scroll the pane under `x` with the wheel until `locator` is on screen, the way a person would. */ +export async function scrollIntoView(page, locator, x, { margin = 40 } = {}) { + const MAX_SCROLLS = 12; + for (let i = 0; i < MAX_SCROLLS; i++) { + const box = await locator.boundingBox(); + if (box && box.y > TOP_SAFE_PX && box.y + box.height < VIEWPORT.height - margin) return; + await moveTo(page, x, VIEWPORT.height / 2, 250); + await wheel(page, box && box.y < TOP_SAFE_PX ? -200 : 200, { steps: 6, ms: 300 }); + } +} diff --git a/scripts/demo-gifs/lib/recorder.mjs b/scripts/demo-gifs/lib/recorder.mjs new file mode 100644 index 00000000..61f2e6fc --- /dev/null +++ b/scripts/demo-gifs/lib/recorder.mjs @@ -0,0 +1,71 @@ +// Captures the workbench with CDP screencast frames, then encodes them to a GIF with ffmpeg. +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { DEVICE_SCALE, FRAMES_DIR, GIF, OUT_DIR, VIEWPORT } from './config.mjs'; +import { sleep } from './vscode.mjs'; + +const JPEG_QUALITY = 95; +const SHEET_COLUMNS = 6; +const SHEET_TILE_WIDTH = 400; + +export class Recorder { + constructor(cdp, name) { + this.cdp = cdp; + this.name = name; + this.dir = path.join(FRAMES_DIR, name); + this.frames = []; + this.onFrame = (event) => this.saveFrame(event); + } + + async saveFrame({ data, metadata, sessionId }) { + const file = path.join(this.dir, `${String(this.frames.length).padStart(5, '0')}.jpg`); + fs.writeFileSync(file, Buffer.from(data, 'base64')); + this.frames.push({ file, time: metadata.timestamp }); + await this.cdp.send('Page.screencastFrameAck', { sessionId }).catch(() => {}); + } + + async start() { + fs.rmSync(this.dir, { recursive: true, force: true }); + fs.mkdirSync(this.dir, { recursive: true }); + this.cdp.on('Page.screencastFrame', this.onFrame); + await this.cdp.send('Page.startScreencast', { + format: 'jpeg', quality: JPEG_QUALITY, + maxWidth: VIEWPORT.width * DEVICE_SCALE, maxHeight: VIEWPORT.height * DEVICE_SCALE, + }); + } + + /** Stop and write an ffmpeg concat list. Frames only arrive on change, so each frame lasts until the next one. */ + async stop(holdSeconds = 2) { + await this.cdp.send('Page.stopScreencast'); + this.cdp.off('Page.screencastFrame', this.onFrame); + await sleep(300); + const entries = this.frames.map((frame, i) => { + const next = this.frames[i + 1]?.time ?? frame.time + holdSeconds; + return `file '${frame.file}'\nduration ${Math.max(0.01, next - frame.time).toFixed(3)}`; + }); + // The concat demuxer ignores the last duration unless the final file is listed again. + fs.writeFileSync(path.join(this.dir, 'concat.txt'), [...entries, `file '${this.frames.at(-1).file}'`].join('\n') + '\n'); + return this.frames.at(-1).time + holdSeconds - this.frames[0].time; + } +} + +export function encodeGif(name) { + fs.mkdirSync(OUT_DIR, { recursive: true }); + const output = path.join(OUT_DIR, `${name}.gif`); + const filters = `fps=${GIF.fps},scale=${GIF.width}:-1:flags=lanczos,split[a][b];` + + `[a]palettegen=max_colors=${GIF.colors}:stats_mode=diff[p];[b][p]paletteuse=dither=sierra2_4a:diff_mode=rectangle`; + execFileSync('ffmpeg', ['-y', '-loglevel', 'error', '-f', 'concat', '-safe', '0', + '-i', path.join(FRAMES_DIR, name, 'concat.txt'), '-vf', filters, '-loop', '0', output]); + return output; +} + +/** One frame per second, tiled, so a person can check every moment for private details before publishing. */ +export function writeReviewSheet(name, durationSeconds) { + const rows = Math.ceil(durationSeconds / SHEET_COLUMNS); + const output = path.join(OUT_DIR, `${name}-review.png`); + execFileSync('ffmpeg', ['-y', '-loglevel', 'error', '-i', path.join(OUT_DIR, `${name}.gif`), + '-vf', `fps=1,scale=${SHEET_TILE_WIDTH}:-1,tile=${SHEET_COLUMNS}x${rows}:padding=4:color=white`, + '-frames:v', '1', output]); + return output; +} diff --git a/scripts/demo-gifs/lib/vscode.mjs b/scripts/demo-gifs/lib/vscode.mjs new file mode 100644 index 00000000..193623bd --- /dev/null +++ b/scripts/demo-gifs/lib/vscode.mjs @@ -0,0 +1,118 @@ +// Launches VS Code with a debug port and drives the workbench through Playwright over CDP. +import { spawn } from 'node:child_process'; +import { chromium } from 'playwright-core'; +import { + DEBUG_PORT, DEVICE_SCALE, EXTENSIONS_DIR, PROFILE_DIR, VIEWPORT, VSCODE_BIN, WORKSPACE_DIR, +} from './config.mjs'; +import { writeWorkspaceFile } from './workspace.mjs'; + +export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const POLL_MS = 250; +const LAUNCH_ATTEMPTS = 120; + +async function poll(attempt, attempts = LAUNCH_ATTEMPTS) { + for (let i = 0; i < attempts; i++) { + const result = await attempt().catch(() => undefined); + if (result) return result; + await sleep(POLL_MS); + } + return undefined; +} + +export async function launchVsCode() { + const child = spawn(VSCODE_BIN, [ + `--remote-debugging-port=${DEBUG_PORT}`, `--user-data-dir=${PROFILE_DIR}`, `--extensions-dir=${EXTENSIONS_DIR}`, + '--disable-workspace-trust', '--skip-welcome', '--skip-release-notes', '--new-window', WORKSPACE_DIR, + ], { stdio: 'ignore' }); + const browser = await poll(() => chromium.connectOverCDP(`http://127.0.0.1:${DEBUG_PORT}`)); + if (!browser) throw new Error(`VS Code never opened its debug port ${DEBUG_PORT}`); + const page = await poll(async () => browser.contexts().flatMap((c) => c.pages()).find((p) => p.url().includes('workbench'))); + if (!page) throw new Error('VS Code workbench window not found'); + await page.waitForSelector('.monaco-workbench', { timeout: 30000 }); + const cdp = await page.context().newCDPSession(page); + // Emulating the viewport (instead of resizing the window) keeps every recording the same size on any screen. + await cdp.send('Emulation.setDeviceMetricsOverride', { ...VIEWPORT, deviceScaleFactor: DEVICE_SCALE, mobile: false }); + await sleep(800); + return { browser, page, cdp, child }; +} + +export async function stopVsCode({ browser, child }) { + await browser.close().catch(() => {}); + // SIGKILL because a modal dialog can make VS Code ignore SIGTERM. The profile is throwaway. + child.kill('SIGKILL'); + await sleep(1000); +} + +const focusedPaletteLabel = (page) => + page.evaluate(() => document.querySelector('.quick-input-list .monaco-list-row.focused')?.getAttribute('aria-label') ?? ''); + +/** Run a Command Palette entry, refusing to press Enter until the exact command is highlighted. */ +export async function runCommand(page, name, { typeText } = {}) { + await page.keyboard.press('Meta+Shift+P'); + await sleep(400); + await (typeText ? typeText(name) : page.keyboard.type(name, { delay: 8 })); + const matches = async () => (await focusedPaletteLabel(page)).toLowerCase().startsWith(name.toLowerCase()); + if (!(await poll(matches, 20))) { + await page.keyboard.press('Escape'); + throw new Error(`Command Palette did not highlight "${name}"`); + } + await page.keyboard.press('Enter'); +} + +const isVisible = (page, selector) => page.evaluate((sel) => { + const el = document.querySelector(sel); + return !!el && el.offsetWidth > 0 && el.offsetHeight > 0; +}, selector); + +/** Only toggle commands exist for some parts, so check visibility first to make the result deterministic. */ +async function hidePart(page, selector, commandName) { + if (!(await isVisible(page, selector))) return; + await runCommand(page, commandName); + await sleep(400); +} + +async function closeEverything(page) { + const MAX_EDITORS = 6; + for (let i = 0; i < MAX_EDITORS; i++) { + const closed = await runCommand(page, 'View: Revert and Close Editor').then(() => true, () => false); + if (!closed) break; + await sleep(300); + } + await runCommand(page, 'View: Close All Editors'); + await sleep(500); + await page.keyboard.press('Escape'); +} + +export async function openFile(page, relativePath) { + await page.keyboard.press('Meta+P'); + await sleep(300); + await page.keyboard.type(relativePath, { delay: 5 }); + await sleep(600); + await page.keyboard.press('Enter'); + await sleep(800); +} + +/** Start each scene from a clean editor: no open tabs, no side bars or panel, one freshly written file. */ +export async function prepareScene(page, { file = '.gitlab-ci.yml', content, cursorAtEnd = true }) { + await closeEverything(page); + await hidePart(page, '.part.sidebar', 'View: Toggle Primary Side Bar Visibility'); + await hidePart(page, '.part.auxiliarybar', 'View: Hide Secondary Side Bar'); + await hidePart(page, '.part.panel', 'View: Toggle Panel Visibility'); + writeWorkspaceFile(file, content); + await openFile(page, file); + if (cursorAtEnd) await page.keyboard.press('Meta+ArrowDown'); + await sleep(2500); +} + +/** Webviews are out-of-process iframes; return the newest live one that contains `selector`. */ +export async function webviewFrame(page, selector) { + const find = async () => { + const live = page.frames().filter((f) => f.url().includes('fake.html') && !f.isDetached()); + const counts = await Promise.all(live.map((f) => f.locator(selector).count().catch(() => 0))); + return live.filter((_, i) => counts[i] > 0).at(-1); + }; + const frame = await poll(find, 80); + if (!frame) throw new Error(`No webview contains ${selector}`); + return frame; +} diff --git a/scripts/demo-gifs/lib/workspace.mjs b/scripts/demo-gifs/lib/workspace.mjs new file mode 100644 index 00000000..1a2b2e8c --- /dev/null +++ b/scripts/demo-gifs/lib/workspace.mjs @@ -0,0 +1,31 @@ +// Builds the throwaway VS Code profile and demo workspace. Nothing here touches the user's own VS Code setup. +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { + DEMO_DIR, EXTENSIONS_DIR, FIXTURES_DIR, PROFILE_DIR, REPO_ROOT, VSCODE_CLI, VSIX_PATH, WORK_DIR, WORKSPACE_DIR, +} from './config.mjs'; + +export function resetWorkDir() { + fs.rmSync(WORK_DIR, { recursive: true, force: true }); + fs.mkdirSync(path.join(PROFILE_DIR, 'User'), { recursive: true }); + fs.mkdirSync(EXTENSIONS_DIR, { recursive: true }); + fs.mkdirSync(WORKSPACE_DIR, { recursive: true }); + fs.copyFileSync(path.join(FIXTURES_DIR, 'settings.json'), path.join(PROFILE_DIR, 'User', 'settings.json')); + fs.cpSync(path.join(FIXTURES_DIR, 'templates'), path.join(WORKSPACE_DIR, 'templates'), { recursive: true }); +} + +/** Package the extension as Marketplace users get it, so the window title has no "[Extension Development Host]". */ +export function installExtension() { + const vsce = path.join(DEMO_DIR, 'node_modules', '.bin', 'vsce'); + execFileSync(vsce, ['package', '--no-dependencies', '-o', VSIX_PATH], { cwd: REPO_ROOT, stdio: 'inherit' }); + execFileSync(VSCODE_CLI, [ + `--user-data-dir=${PROFILE_DIR}`, `--extensions-dir=${EXTENSIONS_DIR}`, '--install-extension', VSIX_PATH, + ], { stdio: 'inherit' }); +} + +export function writeWorkspaceFile(relativePath, content) { + const target = path.join(WORKSPACE_DIR, relativePath); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, content); +} diff --git a/scripts/demo-gifs/package-lock.json b/scripts/demo-gifs/package-lock.json new file mode 100644 index 00000000..cab5251d --- /dev/null +++ b/scripts/demo-gifs/package-lock.json @@ -0,0 +1,1705 @@ +{ + "name": "gitlab-component-helper-demo-gifs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gitlab-component-helper-demo-gifs", + "dependencies": { + "@vscode/vsce": "4.0.0", + "playwright-core": "1.55.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.1.tgz", + "integrity": "sha512-2QygG2F76ZpMP2eMztiJvAiFMu71M9rDeU7vO/QKg5Css7MgM4frUOslFjhVjRhbGaCNPtz/S8M6y46/fFKVuQ==", + "license": "MIT", + "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/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-process": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-process/-/core-process-1.0.0.tgz", + "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "license": "MIT", + "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/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.3.tgz", + "integrity": "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-process": "^1.0.0", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^6.0.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.23.0.tgz", + "integrity": "sha512-IRCwkRCK47hBXK+7bu67UZWY7HS0Jx1dZgi4C1HVbjlBum8Jy8fb2l87xFr6HxdQJWy951zRUHtwwDITHFAtaA==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", + "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.1.tgz", + "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.1", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@napi-rs/keyring": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.3.0.tgz", + "integrity": "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/keyring-darwin-arm64": "1.3.0", + "@napi-rs/keyring-darwin-x64": "1.3.0", + "@napi-rs/keyring-freebsd-x64": "1.3.0", + "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", + "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", + "@napi-rs/keyring-linux-arm64-musl": "1.3.0", + "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-musl": "1.3.0", + "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", + "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", + "@napi-rs/keyring-win32-x64-msvc": "1.3.0" + } + }, + "node_modules/@napi-rs/keyring-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.3.0.tgz", + "integrity": "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-freebsd-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.3.0.tgz", + "integrity": "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.3.0.tgz", + "integrity": "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.3.0.tgz", + "integrity": "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-arm64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.3.0.tgz", + "integrity": "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-ia32-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.3.0.tgz", + "integrity": "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-x64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.3.0.tgz", + "integrity": "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz", + "integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vscode/vsce": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-4.0.0.tgz", + "integrity": "sha512-NImwuLaenMmb5D5Jer9/lzi/F9ZQUBOp8Azhj/BVYcTFgixv8KehFXqEUDjQlD2tAiw2E6dDGyjTuAB//di60A==", + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.13.2", + "@napi-rs/keyring": "^1.3.0", + "@secretlint/core": "^10.2.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.2.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@vscode/vsce-sign": "^2.1.0", + "azure-devops-node-api": "^12.5.0", + "cockatiel": "^3.2.1", + "commander": "^12.1.0", + "hosted-git-info": "^4.1.0", + "jsonc-parser": "^3.3.1", + "marked": "^18.0.11", + "mime": "^1.6.0", + "minimatch": "^10.2.6", + "parse5": "^8.0.1", + "proper-lockfile": "^4.1.2", + "read": "^1.0.7", + "semver": "^7.8.5", + "tinyglobby": "^0.2.17", + "typed-rest-client": "^1.8.11", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.4.0", + "yazl": "^2.5.1" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 22" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.1.0.tgz", + "integrity": "sha512-9AQrqazrBgTgRSuwleLVXUrIUphY02/SFCh2TKYoLV/xifJAdblhdmEmw5gUrYSPQ3sRwNs9iyCMD14sATEE6g==", + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@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" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/marked": { + "version": "18.0.14", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.14.tgz", + "integrity": "sha512-mBHK6FBHuBAlhgRe88w9F0O1AbwwXJUcQibUbC/QcdTbVGAD7aWza+xt3N6oT/jCZx3/OMeS+8rnuiHZcQ9s7A==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "license": "ISC" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "license": "MIT" + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/scripts/demo-gifs/package.json b/scripts/demo-gifs/package.json new file mode 100644 index 00000000..e24697af --- /dev/null +++ b/scripts/demo-gifs/package.json @@ -0,0 +1,13 @@ +{ + "name": "gitlab-component-helper-demo-gifs", + "private": true, + "description": "Records the README demo GIFs by driving a real VS Code. Maintainer tool, never packaged.", + "type": "module", + "scripts": { + "record": "node record.mjs" + }, + "dependencies": { + "@vscode/vsce": "4.0.0", + "playwright-core": "1.55.0" + } +} diff --git a/scripts/demo-gifs/record.mjs b/scripts/demo-gifs/record.mjs new file mode 100644 index 00000000..bb7a1eeb --- /dev/null +++ b/scripts/demo-gifs/record.mjs @@ -0,0 +1,61 @@ +// Re-records the README demo GIFs in a throwaway VS Code profile. Usage and publishing steps live in +// .ai/workflows.yaml (record_demo_gifs). +// +// npm install && npm run record # every scene +// npm run record -- hover versions # just these +import * as browse from './scenes/browse.mjs'; +import * as complete from './scenes/complete.mjs'; +import * as hover from './scenes/hover.mjs'; +import * as validate from './scenes/validate.mjs'; +import * as versions from './scenes/versions.mjs'; +import { showCursor } from './lib/input.mjs'; +import { Recorder, encodeGif, writeReviewSheet } from './lib/recorder.mjs'; +import { launchVsCode, prepareScene, runCommand, sleep, stopVsCode, webviewFrame } from './lib/vscode.mjs'; +import { installExtension, resetWorkDir } from './lib/workspace.mjs'; + +const SCENES = { browse, complete, hover, validate, versions }; +const SOURCE_COUNT = 4; + +/** Open the Component Browser once so every source is cached before the first take. */ +async function warmCache(page) { + await prepareScene(page, { content: 'include:\n' }); + await runCommand(page, 'GitLab CI: Browse Components'); + const list = await webviewFrame(page, '.source-group'); + await list.locator('.source-group').nth(SOURCE_COUNT - 1).waitFor({ timeout: 60000 }); +} + +async function recordScene({ page, cdp }, name) { + const scene = SCENES[name]; + await scene.prepare(page); + await showCursor(page, scene.cursorStart.x, scene.cursorStart.y); + const recorder = new Recorder(cdp, name); + await recorder.start(); + await sleep(1000); + await scene.perform(page); + const duration = await recorder.stop(); + const gif = encodeGif(name); + const sheet = writeReviewSheet(name, duration); + console.log(`${name}: ${gif} (${duration.toFixed(1)}s), review sheet ${sheet}`); +} + +async function main() { + const requested = process.argv.slice(2); + const unknown = requested.filter((name) => !SCENES[name]); + if (unknown.length) throw new Error(`Unknown scene(s): ${unknown.join(', ')}. Pick from ${Object.keys(SCENES).join(', ')}`); + const names = requested.length ? requested : Object.keys(SCENES); + + resetWorkDir(); + installExtension(); + const session = await launchVsCode(); + try { + await warmCache(session.page); + for (const name of names) await recordScene(session, name); + } finally { + await stopVsCode(session); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/demo-gifs/scenes/browse.mjs b/scripts/demo-gifs/scenes/browse.mjs new file mode 100644 index 00000000..98863940 --- /dev/null +++ b/scripts/demo-gifs/scenes/browse.mjs @@ -0,0 +1,42 @@ +// Component Browser: search, open details, pick inputs, insert, then show the result full width. +import { centerOf, clickOn, moveTo, scrollIntoView, typeSlow } from '../lib/input.mjs'; +import { prepareScene, runCommand, sleep, webviewFrame } from '../lib/vscode.mjs'; + +export const cursorStart = { x: 700, y: 520 }; + +export async function prepare(page) { + await prepareScene(page, { content: 'stages:\n - test\n\ninclude:\n' }); +} + +const tabClose = (page, label) => page.locator(`.tab[aria-label^="${label}"] .tab-actions .action-label`).first(); + +export async function perform(page) { + await runCommand(page, 'GitLab CI: Browse Components', { typeText: (t) => typeSlow(page, t, 45) }); + const list = await webviewFrame(page, '#search'); + await sleep(1500); + await clickOn(page, list.locator('#search')); + await typeSlow(page, 'secret', 110); + await sleep(1500); + await clickOn(page, list.locator('.component-card[data-name="secret-detection"] [data-action="viewDetails"]')); + const details = await webviewFrame(page, '#parametersContainer .parameter'); + await sleep(1500); + // Closing the list leaves editor | details side by side, which reads better than three narrow columns. + await clickOn(page, tabClose(page, 'GitLab CI/CD Components'), { ms: 700 }); + await moveTo(page, 900, 300, 400); + await sleep(1100); + const panelX = (await centerOf(details.locator('#componentName'))).x; + for (const input of ['stage', 'historic_scan', 'excluded_paths']) { + const checkbox = details.locator(`#input-${input}`); + await scrollIntoView(page, checkbox, panelX); + await clickOn(page, checkbox, { ms: 450 }); + await sleep(450); + } + const insert = details.locator('button[data-action="insertComponent"]'); + await scrollIntoView(page, insert, panelX); + await sleep(400); + await clickOn(page, insert); + await sleep(2200); + await clickOn(page, tabClose(page, 'Component: secret-detection'), { ms: 800 }); + await moveTo(page, 820, 520, 500); + await sleep(3000); +} diff --git a/scripts/demo-gifs/scenes/complete.mjs b/scripts/demo-gifs/scenes/complete.mjs new file mode 100644 index 00000000..731448a5 --- /dev/null +++ b/scripts/demo-gifs/scenes/complete.mjs @@ -0,0 +1,45 @@ +// Completion: component names after `component:`, then the version list after `@`. +import { typeSlow } from '../lib/input.mjs'; +import { prepareScene, sleep } from '../lib/vscode.mjs'; + +export const cursorStart = { x: 1100, y: 640 }; + +export async function prepare(page) { + await prepareScene(page, { content: 'stages:\n - validate\n - build\n - deploy\n\ninclude:\n' }); +} + +async function press(page, key, times = 1, gap = 90) { + for (let i = 0; i < times; i++) { + await page.keyboard.press(key); + await sleep(gap); + } +} + +export async function perform(page) { + await typeSlow(page, ' - component: ', 75); + await sleep(1800); + await typeSlow(page, 'plan', 160); + await sleep(1800); + await press(page, 'ArrowDown', 1, 500); + await press(page, 'ArrowUp', 1, 700); + await page.keyboard.press('Enter'); + await sleep(1500); + // Drop the version that came with the suggestion to show the version picker. + await press(page, 'Backspace', 5); + await sleep(600); + await page.keyboard.press('Control+Space'); + await sleep(1800); + await press(page, 'ArrowDown', 1, 450); + await press(page, 'ArrowUp', 1, 600); + await page.keyboard.press('Enter'); + await sleep(1200); + await page.keyboard.press('End'); + await page.keyboard.press('Enter'); + // Auto-indent already lines the new entry up under the first one. + await typeSlow(page, '- component: ', 60); + await sleep(1200); + await typeSlow(page, 'secret', 140); + await sleep(1300); + await page.keyboard.press('Enter'); + await sleep(2500); +} diff --git a/scripts/demo-gifs/scenes/hover.mjs b/scripts/demo-gifs/scenes/hover.mjs new file mode 100644 index 00000000..0db58052 --- /dev/null +++ b/scripts/demo-gifs/scenes/hover.mjs @@ -0,0 +1,56 @@ +// Hover: component docs with version status, an input's docs, and an outdated pin. +import { moveTo, wordPosition } from '../lib/input.mjs'; +import { prepareScene, sleep } from '../lib/vscode.mjs'; + +export const cursorStart = { x: 1000, y: 650 }; +const REST = { x: 1000, y: 650 }; +const URL_OFFSET_PX = 120; + +export async function prepare(page) { + await prepareScene(page, { + cursorAtEnd: false, + content: `stages: + - validate + - build + - test + +include: + - component: gitlab.com/components/opentofu/validate-plan@4.9.0 + inputs: + opentofu_version: 1.12.6 + root_dir: infra/ + - component: gitlab.com/components/sast/sast@3.3.0 + inputs: + excluded_paths: "spec, tmp" +`, + }); + // The version check runs on open/save, so save once to make the outdated pin's hint ready before recording. + await page.keyboard.press('Meta+S'); + await sleep(4000); +} + +async function hoverWord(page, lineText, word, { offset = 0, hold = 3500 } = {}) { + const { x, y } = await wordPosition(page, lineText, word); + await moveTo(page, x + offset, y, 850); + await sleep(hold); +} + +export async function perform(page) { + await hoverWord(page, 'opentofu/validate-plan', 'gitlab.com', { offset: URL_OFFSET_PX }); + const hover = page.locator('.monaco-hover:visible .monaco-scrollable-element').first(); + const box = await hover.boundingBox(); + await moveTo(page, box.x + box.width / 2, box.y + box.height / 2, 600); + for (let i = 0; i < 8; i++) { + await page.mouse.wheel(0, 60); + await sleep(180); + } + await sleep(2000); + await moveTo(page, REST.x, REST.y, 500); + await sleep(600); + await hoverWord(page, 'opentofu_version', 'opentofu_version'); + await moveTo(page, REST.x, REST.y, 500); + await sleep(600); + await hoverWord(page, 'sast/sast', 'gitlab.com', { offset: URL_OFFSET_PX, hold: 4000 }); + await moveTo(page, 1100, 700, 600); + await sleep(800); +} diff --git a/scripts/demo-gifs/scenes/validate.mjs b/scripts/demo-gifs/scenes/validate.mjs new file mode 100644 index 00000000..68ff56ec --- /dev/null +++ b/scripts/demo-gifs/scenes/validate.mjs @@ -0,0 +1,77 @@ +// Input checks: Quick Fix a mistyped remote input, then fill a local template's inputs until its error clears. +import { moveTo, typeSlow, wordPosition } from '../lib/input.mjs'; +import { prepareScene, sleep } from '../lib/vscode.mjs'; + +export const cursorStart = { x: 1100, y: 650 }; +const REST = { x: 1100, y: 650 }; + +export async function prepare(page) { + await prepareScene(page, { + content: `stages: + - test + - deploy + +include: + - component: gitlab.com/components/sast/sast@3.5.0 + inputs: + excluded_path: "spec, tmp" +`, + }); + await page.keyboard.press('Meta+S'); + await sleep(3000); +} + +async function pickSuggestion(page, downPresses) { + await page.keyboard.press('Control+Space'); + await sleep(1500); + for (let i = 0; i < downPresses; i++) { + await page.keyboard.press('ArrowDown'); + await sleep(550); + } + await page.keyboard.press('Enter'); + await sleep(500); +} + +async function fixTypo(page) { + const typo = await wordPosition(page, 'excluded_path', 'excluded_path'); + await moveTo(page, typo.x, typo.y, 900); + await sleep(2800); + await page.mouse.click(typo.x, typo.y); + await sleep(300); + await page.keyboard.press('Meta+.'); + await sleep(1500); + // The suggestions aren't ranked yet (#333), so walk down to `excluded_paths`. + for (let i = 0; i < 2; i++) { + await page.keyboard.press('ArrowDown'); + await sleep(500); + } + await sleep(500); + await page.keyboard.press('Enter'); + await moveTo(page, REST.x, REST.y, 500); + await sleep(1200); +} + +export async function perform(page) { + await fixTypo(page); + await page.keyboard.press('Meta+ArrowDown'); + await typeSlow(page, ' - local: templates/deploy.yml', 55); + await page.keyboard.press('Enter'); + await typeSlow(page, ' inputs:', 60); + await page.keyboard.press('Enter'); + await typeSlow(page, ' ', 60); + await pickSuggestion(page, 1); // environment + await pickSuggestion(page, 1); // production + await page.keyboard.press('Meta+S'); + await sleep(2000); + const local = await wordPosition(page, 'local:', 'templates/deploy.yml'); + await moveTo(page, local.x, local.y, 900); + await sleep(3500); + await moveTo(page, REST.x, REST.y, 500); + await page.keyboard.press('Meta+ArrowDown'); + await page.keyboard.press('End'); + await page.keyboard.press('Enter'); + await pickSuggestion(page, 0); // app_name, the only missing input left + await typeSlow(page, 'billing-api', 90); + await page.keyboard.press('Meta+S'); + await sleep(3000); +} diff --git a/scripts/demo-gifs/scenes/versions.mjs b/scripts/demo-gifs/scenes/versions.mjs new file mode 100644 index 00000000..09a5969b --- /dev/null +++ b/scripts/demo-gifs/scenes/versions.mjs @@ -0,0 +1,43 @@ +// Version upgrades: hover an outdated pin, Quick Fix it, then update the rest from the Command Palette. +import { moveTo, typeSlow, wordPosition } from '../lib/input.mjs'; +import { prepareScene, runCommand, sleep } from '../lib/vscode.mjs'; + +export const cursorStart = { x: 1100, y: 650 }; + +export async function prepare(page) { + await prepareScene(page, { + cursorAtEnd: false, + content: `stages: + - validate + - build + - test + - deploy + +include: + - component: gitlab.com/components/opentofu/validate-plan-apply@4.6.0 + - component: gitlab.com/components/sast/sast@3.3.0 + - component: gitlab.com/components/secret-detection/secret-detection@2.1.0 + - component: gitlab.com/components/code-quality/code-quality@1.0.6 +`, + }); + await page.keyboard.press('Meta+S'); + await sleep(5000); +} + +export async function perform(page) { + const pin = await wordPosition(page, 'sast/sast', '3.3.0'); + await moveTo(page, pin.x, pin.y, 900); + await sleep(3800); + await page.mouse.click(pin.x, pin.y); + await sleep(300); + await page.keyboard.press('Meta+.'); + await sleep(1700); + await page.keyboard.press('Enter'); + await sleep(1200); + await moveTo(page, 1100, 650, 600); + await sleep(800); + await runCommand(page, 'GitLab CI: Update All Component Versions to Latest', { typeText: (t) => typeSlow(page, t, 45) }); + await sleep(3500); + await page.keyboard.press('Meta+S'); + await sleep(2500); +} From 5e35a59ec4c18b0fb4ef5f815305492f967f9258 Mon Sep 17 00:00:00 2001 From: eFAILution Date: Sun, 4 Oct 2026 10:41:25 -0400 Subject: [PATCH 2/2] fix(demo): guard the work dir and attach to the right VS Code - Refuse a GCH_DEMO_DIR not named gch-demo* or containing the home folder or repo, since the folder is wiped on every run. - Launch on a free debug port read from DevToolsActivePort, and kill VS Code if attaching fails, so a stray instance is never recorded. - Delete back to the @ however long the inserted version is. - Read the source count from the settings fixture. - Move the real mouse with the drawn cursor so hovers start in place. - Document npm ci, the root build prerequisite, and the folder rule. --- .ai/architecture.yaml | 6 +++-- .ai/errors.yaml | 7 ++--- .ai/workflows.yaml | 6 +++-- scripts/demo-gifs/lib/config.mjs | 17 ++++++++++-- scripts/demo-gifs/lib/input.mjs | 1 + scripts/demo-gifs/lib/vscode.mjs | 37 +++++++++++++++++++-------- scripts/demo-gifs/lib/workspace.mjs | 4 +++ scripts/demo-gifs/record.mjs | 4 +-- scripts/demo-gifs/scenes/complete.mjs | 3 ++- 9 files changed, 63 insertions(+), 22 deletions(-) diff --git a/.ai/architecture.yaml b/.ai/architecture.yaml index 3c36da84..44b0f847 100644 --- a/.ai/architecture.yaml +++ b/.ai/architecture.yaml @@ -149,7 +149,8 @@ components: packaged: false (scripts/** in .vscodeignore; own package.json, out/ and node_modules/ gitignored) files: record.mjs: Entry point. Resets the work dir, packages + installs the VSIX, warms the cache, records scenes - lib/config.mjs: Paths, viewport (1280x800 at 2x), GIF size (1100 wide, 12 fps), env overrides + lib/config.mjs: Paths, viewport (1280x800 at 2x), GIF size (1100 wide, 12 fps), env overrides; refuses a + GCH_DEMO_DIR not named gch-demo* or containing the home folder or repo, since it is wiped each run lib/workspace.mjs: Throwaway profile, demo workspace (my-pipelines), vsce package + install lib/vscode.mjs: Launch/stop VS Code, Command Palette, scene reset, webview frame lookup lib/input.mjs: Drawn cursor, eased mouse moves, typing, wheel scrolling @@ -157,7 +158,8 @@ components: scenes/: One module per GIF (browse, complete, hover, validate, versions) fixtures/: Profile settings.json (component sources, quiet editor) and templates/deploy.yml for local includes how_it_works: - - VS Code starts with --remote-debugging-port; Playwright attaches with chromium.connectOverCDP + - VS Code starts with --remote-debugging-port=0; the port is read from /DevToolsActivePort and + Playwright attaches with chromium.connectOverCDP, so a stray instance can't be picked up by mistake - Emulation.setDeviceMetricsOverride fixes the viewport so output size doesn't depend on the screen - The OS pointer isn't captured, so a cursor element is drawn in the top document and moved with page.mouse - Page.startScreencast frames only arrive on change; an ffmpeg concat list holds each frame until the next diff --git a/.ai/errors.yaml b/.ai/errors.yaml index 8319b5b3..f9e40861 100644 --- a/.ai/errors.yaml +++ b/.ai/errors.yaml @@ -395,11 +395,12 @@ errors: symptom: npm run record in scripts/demo-gifs fails or produces a wrong-looking GIF causes: - cause: 'listen EINVAL on .../1.13-main.sock or "IPC handle is longer than 103 chars"' - solution: Point GCH_DEMO_DIR at a short path (default /tmp/gch-demo) + solution: Point GCH_DEMO_DIR at a short folder named gch-demo*, e.g. /tmp/gch-demo2 (default /tmp/gch-demo). + Other names are refused because the folder is wiped on every run - cause: 'mach_port_rendezvous ... Permission denied, or "Starting inspector failed: operation not permitted"' solution: Run outside the command sandbox; VS Code can't start inside it - - cause: VS Code never opened its debug port, or a stray instance holds the profile - solution: pkill -9 -f "user-data-dir=/tmp/gch-demo/profile" and run again + - cause: VS Code never wrote DevToolsActivePort, or a stray instance from a crashed run holds the profile + solution: pkill -9 -f "user-data-dir=${GCH_DEMO_DIR:-/tmp/gch-demo}/profile" and run again - cause: Command Palette did not highlight a command solution: The command title changed in VS Code; update the name in the scene or lib/vscode.mjs - cause: An empty "Drag a view here" pane or a side bar shows up diff --git a/.ai/workflows.yaml b/.ai/workflows.yaml index af7b5103..adbb2576 100644 --- a/.ai/workflows.yaml +++ b/.ai/workflows.yaml @@ -146,14 +146,16 @@ workflows: - ffmpeg on PATH - Network access to gitlab.com (scenes use public components/opentofu, sast, secret-detection, code-quality) - Run outside any command sandbox; VS Code needs its mach port and debug port + - Root dependencies installed (npm ci at the repo root); vsce package runs the root vscode:prepublish build steps: - action: install_recorder_dependencies - command: cd scripts/demo-gifs && npm install + command: cd scripts/demo-gifs && npm ci note: Own package.json/lock, separate from the extension's; root npm install never touches it - action: record command: npm run record note: 'All scenes, or name some: npm run record -- hover versions. Packages the extension with vsce, - installs it into a throwaway profile under /tmp/gch-demo (GCH_DEMO_DIR), drives VS Code over CDP, + installs it into a throwaway profile under /tmp/gch-demo (GCH_DEMO_DIR, which is wiped each run and must be + named gch-demo*), drives VS Code over CDP on a free debug port, writes out/.gif and out/-review.png' - action: privacy_review check: Open every out/-review.png (one tile per second) and confirm no names, paths, accounts, diff --git a/scripts/demo-gifs/lib/config.mjs b/scripts/demo-gifs/lib/config.mjs index afdeb37f..1230a81b 100644 --- a/scripts/demo-gifs/lib/config.mjs +++ b/scripts/demo-gifs/lib/config.mjs @@ -1,3 +1,4 @@ +import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -6,8 +7,21 @@ export const REPO_ROOT = path.resolve(DEMO_DIR, '../..'); export const FIXTURES_DIR = path.join(DEMO_DIR, 'fixtures'); export const OUT_DIR = path.join(DEMO_DIR, 'out'); +const WORK_DIR_PREFIX = 'gch-demo'; + +/** The work dir is wiped on every run, so only accept a folder whose own name marks it as ours. */ +function resolveWorkDir(requested) { + const dir = path.resolve(requested); + const isOurs = path.basename(dir).startsWith(WORK_DIR_PREFIX); + const holdsSomethingPrecious = [os.homedir(), REPO_ROOT].some((p) => !path.relative(dir, p).startsWith('..')); + if (!isOurs || holdsSomethingPrecious) { + throw new Error(`GCH_DEMO_DIR must be a throwaway folder named ${WORK_DIR_PREFIX}*, got ${dir}`); + } + return dir; +} + // VS Code's IPC socket lives in the profile and its path must stay under ~103 characters, so keep this short. -export const WORK_DIR = process.env.GCH_DEMO_DIR ?? '/tmp/gch-demo'; +export const WORK_DIR = resolveWorkDir(process.env.GCH_DEMO_DIR ?? '/tmp/gch-demo'); export const PROFILE_DIR = path.join(WORK_DIR, 'profile'); export const EXTENSIONS_DIR = path.join(WORK_DIR, 'extensions'); // The folder name shows in the window title, so it doubles as the demo's project name. @@ -18,7 +32,6 @@ export const VSIX_PATH = path.join(WORK_DIR, 'extension.vsix'); const VSCODE_APP = '/Applications/Visual Studio Code.app/Contents'; export const VSCODE_BIN = process.env.VSCODE_BIN ?? `${VSCODE_APP}/MacOS/Code`; export const VSCODE_CLI = process.env.VSCODE_CLI ?? `${VSCODE_APP}/Resources/app/bin/code`; -export const DEBUG_PORT = Number(process.env.GCH_DEMO_PORT ?? 9333); export const VIEWPORT = { width: 1280, height: 800 }; export const DEVICE_SCALE = 2; diff --git a/scripts/demo-gifs/lib/input.mjs b/scripts/demo-gifs/lib/input.mjs index feea836e..fd7b2650 100644 --- a/scripts/demo-gifs/lib/input.mjs +++ b/scripts/demo-gifs/lib/input.mjs @@ -18,6 +18,7 @@ export async function showCursor(page, x, y) { el.style.cssText = `position:fixed;left:0;top:0;width:22px;height:28px;z-index:2147483647;pointer-events:none;transform:translate(${x}px,${y}px);background:url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}") no-repeat`; document.body.appendChild(el); }, { id: CURSOR_ID, svg: CURSOR_SVG, x, y }); + await page.mouse.move(x, y); cursor = { x, y }; } diff --git a/scripts/demo-gifs/lib/vscode.mjs b/scripts/demo-gifs/lib/vscode.mjs index 193623bd..618cdbf7 100644 --- a/scripts/demo-gifs/lib/vscode.mjs +++ b/scripts/demo-gifs/lib/vscode.mjs @@ -1,9 +1,9 @@ // Launches VS Code with a debug port and drives the workbench through Playwright over CDP. +import fs from 'node:fs'; +import path from 'node:path'; import { spawn } from 'node:child_process'; import { chromium } from 'playwright-core'; -import { - DEBUG_PORT, DEVICE_SCALE, EXTENSIONS_DIR, PROFILE_DIR, VIEWPORT, VSCODE_BIN, WORKSPACE_DIR, -} from './config.mjs'; +import { DEVICE_SCALE, EXTENSIONS_DIR, PROFILE_DIR, VIEWPORT, VSCODE_BIN, WORKSPACE_DIR } from './config.mjs'; import { writeWorkspaceFile } from './workspace.mjs'; export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -20,13 +20,17 @@ async function poll(attempt, attempts = LAUNCH_ATTEMPTS) { return undefined; } -export async function launchVsCode() { - const child = spawn(VSCODE_BIN, [ - `--remote-debugging-port=${DEBUG_PORT}`, `--user-data-dir=${PROFILE_DIR}`, `--extensions-dir=${EXTENSIONS_DIR}`, - '--disable-workspace-trust', '--skip-welcome', '--skip-release-notes', '--new-window', WORKSPACE_DIR, - ], { stdio: 'ignore' }); - const browser = await poll(() => chromium.connectOverCDP(`http://127.0.0.1:${DEBUG_PORT}`)); - if (!browser) throw new Error(`VS Code never opened its debug port ${DEBUG_PORT}`); +/** Port 0 lets Chromium pick a free port and write it here, so we always attach to the copy we just started. */ +async function readDebugPort() { + const file = path.join(PROFILE_DIR, 'DevToolsActivePort'); + return poll(async () => Number(fs.readFileSync(file, 'utf8').split('\n')[0]) || undefined); +} + +async function attach(child) { + const port = await readDebugPort(); + if (!port) throw new Error('VS Code never wrote DevToolsActivePort'); + const browser = await poll(() => chromium.connectOverCDP(`http://127.0.0.1:${port}`)); + if (!browser) throw new Error(`VS Code never answered on debug port ${port}`); const page = await poll(async () => browser.contexts().flatMap((c) => c.pages()).find((p) => p.url().includes('workbench'))); if (!page) throw new Error('VS Code workbench window not found'); await page.waitForSelector('.monaco-workbench', { timeout: 30000 }); @@ -37,6 +41,19 @@ export async function launchVsCode() { return { browser, page, cdp, child }; } +export async function launchVsCode() { + const child = spawn(VSCODE_BIN, [ + '--remote-debugging-port=0', `--user-data-dir=${PROFILE_DIR}`, `--extensions-dir=${EXTENSIONS_DIR}`, + '--disable-workspace-trust', '--skip-welcome', '--skip-release-notes', '--new-window', WORKSPACE_DIR, + ], { stdio: 'ignore' }); + try { + return await attach(child); + } catch (error) { + child.kill('SIGKILL'); + throw error; + } +} + export async function stopVsCode({ browser, child }) { await browser.close().catch(() => {}); // SIGKILL because a modal dialog can make VS Code ignore SIGTERM. The profile is throwaway. diff --git a/scripts/demo-gifs/lib/workspace.mjs b/scripts/demo-gifs/lib/workspace.mjs index 1a2b2e8c..6c0c54c9 100644 --- a/scripts/demo-gifs/lib/workspace.mjs +++ b/scripts/demo-gifs/lib/workspace.mjs @@ -24,6 +24,10 @@ export function installExtension() { ], { stdio: 'inherit' }); } +export function readFixtureSettings() { + return JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, 'settings.json'), 'utf8')); +} + export function writeWorkspaceFile(relativePath, content) { const target = path.join(WORKSPACE_DIR, relativePath); fs.mkdirSync(path.dirname(target), { recursive: true }); diff --git a/scripts/demo-gifs/record.mjs b/scripts/demo-gifs/record.mjs index bb7a1eeb..0a3ae7b6 100644 --- a/scripts/demo-gifs/record.mjs +++ b/scripts/demo-gifs/record.mjs @@ -11,10 +11,10 @@ import * as versions from './scenes/versions.mjs'; import { showCursor } from './lib/input.mjs'; import { Recorder, encodeGif, writeReviewSheet } from './lib/recorder.mjs'; import { launchVsCode, prepareScene, runCommand, sleep, stopVsCode, webviewFrame } from './lib/vscode.mjs'; -import { installExtension, resetWorkDir } from './lib/workspace.mjs'; +import { installExtension, readFixtureSettings, resetWorkDir } from './lib/workspace.mjs'; const SCENES = { browse, complete, hover, validate, versions }; -const SOURCE_COUNT = 4; +const SOURCE_COUNT = readFixtureSettings()['gitlabComponentHelper.componentSources'].length; /** Open the Component Browser once so every source is cached before the first take. */ async function warmCache(page) { diff --git a/scripts/demo-gifs/scenes/complete.mjs b/scripts/demo-gifs/scenes/complete.mjs index 731448a5..28185b19 100644 --- a/scripts/demo-gifs/scenes/complete.mjs +++ b/scripts/demo-gifs/scenes/complete.mjs @@ -25,7 +25,8 @@ export async function perform(page) { await page.keyboard.press('Enter'); await sleep(1500); // Drop the version that came with the suggestion to show the version picker. - await press(page, 'Backspace', 5); + const line = await page.locator('.view-line', { hasText: 'opentofu/plan@' }).first().innerText(); + await press(page, 'Backspace', line.trimEnd().length - line.lastIndexOf('@') - 1); await sleep(600); await page.keyboard.press('Control+Space'); await sleep(1800);