Download apps/vscode/src/wizardPanel.ts from smlflg/Harness_swap: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/smlflg/Harness_swap/resolve/main/apps/vscode/src/wizardPanel.ts
- Command line
-
hf download hf://smlflg/Harness_swap/apps/vscode/src/wizardPanel.ts
-
curl -L -o wizardPanel.ts https://huggingface.co/smlflg/Harness_swap/resolve/main/apps/vscode/src/wizardPanel.ts
16.1 kB
| import * as path from "node:path"; | |
| import * as vscode from "vscode"; | |
| import type { MigrationResult } from "@harness-swap/schema"; | |
| import { buildZipArchive, exportFileName, runMigration } from "@harness-swap/migrate-core"; | |
| import type { WorkspaceFolder } from "vscode"; | |
| import { WorkspaceSwapAdapter } from "./adapters/workspaceFs"; | |
| import { | |
| asMigrationOptions, | |
| isReadyMessage, | |
| isRunStartMessage, | |
| isStopRunMessage, | |
| type HostLogMessage, | |
| type HostOutgoingMessage, | |
| type HostProgressMessage, | |
| type WebviewIncomingMessage, | |
| WizardHostMessageType | |
| } from "./protocol/messages"; | |
| import type { OutputChannel } from "vscode"; | |
| interface WorkspaceFolderChoice { | |
| uri: string; | |
| name: string; | |
| } | |
| export class WizardPanel { | |
| public static readonly viewType = "harnessSwap.wizard"; | |
| private static current: WizardPanel | undefined; | |
| public static createOrShow(context: vscode.ExtensionContext, outputChannel: OutputChannel): WizardPanel { | |
| if (WizardPanel.current) { | |
| WizardPanel.current.panel.reveal(vscode.ViewColumn.Beside); | |
| return WizardPanel.current; | |
| } | |
| const folderChoices: WorkspaceFolderChoice[] = | |
| vscode.workspace.workspaceFolders?.map((folder: WorkspaceFolder) => ({ | |
| uri: folder.uri.fsPath, | |
| name: folder.name | |
| })) ?? []; | |
| const panel = vscode.window.createWebviewPanel( | |
| WizardPanel.viewType, | |
| "Harness Swap Migration Wizard", | |
| vscode.ViewColumn.One, | |
| { | |
| enableScripts: true, | |
| localResourceRoots: [context.extensionUri], | |
| retainContextWhenHidden: false | |
| } | |
| ); | |
| const instance = new WizardPanel(panel, folderChoices, outputChannel); | |
| WizardPanel.current = instance; | |
| return instance; | |
| } | |
| public static currentPanel(): WizardPanel | undefined { | |
| return WizardPanel.current; | |
| } | |
| private readonly adapter: WorkspaceSwapAdapter; | |
| private readonly disposables: vscode.Disposable[] = []; | |
| private activeRun: AbortController | undefined; | |
| private constructor( | |
| private readonly panel: vscode.WebviewPanel, | |
| private readonly workspaceFolders: WorkspaceFolderChoice[], | |
| private readonly outputChannel: OutputChannel | |
| ) { | |
| this.adapter = new WorkspaceSwapAdapter({ | |
| log: (level, message) => { | |
| const levelPrefix = level.toUpperCase().padEnd(7, " "); | |
| this.outputChannel.appendLine(`[${levelPrefix}] ${message}`); | |
| } | |
| }); | |
| this.panel.webview.html = this.renderHtml(this.workspaceFolders); | |
| this.panel.onDidDispose(() => this.dispose(), null, this.disposables); | |
| this.panel.webview.onDidReceiveMessage((message: WebviewIncomingMessage) => void this.onMessage(message), this, this.disposables); | |
| } | |
| public dispose(): void { | |
| this.activeRun?.abort(); | |
| this.panel.dispose(); | |
| for (const disposable of this.disposables.splice(0)) { | |
| disposable.dispose(); | |
| } | |
| if (WizardPanel.current === this) { | |
| WizardPanel.current = undefined; | |
| } | |
| } | |
| private async onMessage(message: WebviewIncomingMessage): Promise<void> { | |
| if (isReadyMessage(message)) { | |
| this.postHost({ type: WizardHostMessageType.LOG, level: "info", message: "Wizard is ready." }); | |
| return; | |
| } | |
| if (isStopRunMessage(message)) { | |
| if (!this.activeRun) { | |
| this.postHost({ type: WizardHostMessageType.LOG, level: "warning", message: "No active run to stop." }); | |
| return; | |
| } | |
| this.activeRun.abort(); | |
| this.postHost({ type: WizardHostMessageType.LOG, level: "warning", message: "Migration stop requested." }); | |
| return; | |
| } | |
| if (!isRunStartMessage(message)) { | |
| this.postHost({ | |
| type: WizardHostMessageType.ERROR, | |
| message: "Received malformed wizard message." | |
| }); | |
| return; | |
| } | |
| if (this.activeRun) { | |
| this.postHost({ | |
| type: WizardHostMessageType.ERROR, | |
| message: "A migration is already running. Please stop it before starting another." | |
| }); | |
| return; | |
| } | |
| await this.executeMigration(message.sourceRoot, message.outputRoot, message.packageName, message.options); | |
| } | |
| private async executeMigration( | |
| sourceRoot: string, | |
| outputRoot: string | undefined, | |
| packageName: string | undefined, | |
| options: Parameters<typeof asMigrationOptions>[0] | |
| ): Promise<void> { | |
| if (!sourceRoot) { | |
| this.error("A source root is required."); | |
| return; | |
| } | |
| this.activeRun = new AbortController(); | |
| const signal = this.activeRun.signal; | |
| try { | |
| this.postHost({ | |
| type: WizardHostMessageType.LOG, | |
| level: "info", | |
| message: `Starting migration from ${sourceRoot}` | |
| }); | |
| const migrationOptions = asMigrationOptions(options); | |
| this.postProgress("discover", "Collecting workspace files"); | |
| const files = await this.adapter.listFiles(sourceRoot, signal); | |
| signal.throwIfAborted(); | |
| this.postHost({ | |
| type: WizardHostMessageType.PROGRESS, | |
| phase: "transform", | |
| runId: "pending", | |
| detail: `Preparing migration from ${files.length} files` | |
| }); | |
| const result = await runMigration(files, migrationOptions); | |
| signal.throwIfAborted(); | |
| const runId = result.runId; | |
| const userOutputRoot = | |
| outputRoot && outputRoot.length > 0 | |
| ? (path.isAbsolute(outputRoot) ? path.normalize(outputRoot) : path.resolve(sourceRoot, outputRoot)) | |
| : path.join(sourceRoot, ".harness-swap"); | |
| const outputRootIsInsideWorkspace = | |
| outputRoot === undefined || | |
| path.isAbsolute(outputRoot) || | |
| path.resolve(sourceRoot) === path.resolve(userOutputRoot) || | |
| isDescendant(path.resolve(sourceRoot), path.resolve(userOutputRoot)); | |
| if (!outputRootIsInsideWorkspace) { | |
| this.error("Output root must be inside the selected source workspace or an absolute path."); | |
| return; | |
| } | |
| const sanitizedName = (packageName ?? "").trim().replaceAll("\\", "_").replaceAll("/", "_"); | |
| const safePackageName = ensureZipExtension(sanitizedName.length > 0 ? sanitizedName : exportFileName(result)); | |
| const outputPackage = path.join(userOutputRoot, safePackageName); | |
| this.postProgress("package", `Writing package to ${outputPackage}`); | |
| const archiveBytes = await buildZipArchive(result.files); | |
| await this.adapter.writeFile(outputPackage, archiveBytes, signal); | |
| signal.throwIfAborted(); | |
| this.postProgress("done", `Done. Created ZIP with ${result.files.length} files.`); | |
| this.logResult(result, outputPackage); | |
| this.postResult(result, outputPackage); | |
| this.postHost({ | |
| type: WizardHostMessageType.LOG, | |
| level: "info", | |
| message: `Migration complete: ${outputPackage}` | |
| }); | |
| } catch (err) { | |
| if ((err as Error).name === "AbortError") { | |
| this.postHost({ | |
| type: WizardHostMessageType.ERROR, | |
| message: "Migration was cancelled." | |
| }); | |
| } else { | |
| const message = err instanceof Error ? err.message : String(err); | |
| this.error(message); | |
| } | |
| } finally { | |
| this.activeRun = undefined; | |
| this.postHost({ | |
| type: WizardHostMessageType.LOG, | |
| level: "debug", | |
| message: "Run loop complete." | |
| }); | |
| } | |
| } | |
| private logResult(result: MigrationResult, outputPackage: string): void { | |
| for (const event of result.events) { | |
| const line = `[${result.runId}] ${event.step}: ${event.message}`; | |
| const level: HostLogMessage["level"] = | |
| event.severity === "warning" | |
| ? "warning" | |
| : event.severity === "error" | |
| ? "error" | |
| : event.severity === "debug" | |
| ? "debug" | |
| : "info"; | |
| this.postHost({ type: WizardHostMessageType.LOG, level, message: `${line} (${outputPackage})` }); | |
| } | |
| } | |
| private postHost(message: HostOutgoingMessage): void { | |
| void this.panel.webview.postMessage(message); | |
| } | |
| private postProgress(phase: HostProgressMessage["phase"], detail: string): void { | |
| this.postHost({ type: WizardHostMessageType.PROGRESS, phase, detail }); | |
| } | |
| private postResult(result: MigrationResult, outputPackage: string): void { | |
| this.postHost({ | |
| type: WizardHostMessageType.RESULT, | |
| runId: result.runId, | |
| outputFolder: outputPackage, | |
| fileCount: result.files.length | |
| }); | |
| } | |
| private error(message: string): void { | |
| this.postHost({ | |
| type: WizardHostMessageType.ERROR, | |
| message | |
| }); | |
| this.outputChannel.appendLine(`[ERROR] ${message}`); | |
| } | |
| private renderHtml(folderChoices: WorkspaceFolderChoice[]): string { | |
| const hasWorkspaceFolders = folderChoices.length > 0; | |
| const hasSourceOption = hasWorkspaceFolders ? "true" : "false"; | |
| const optionsMarkup = folderChoices.length > 0 | |
| ? folderChoices | |
| .map( | |
| (folder) => | |
| `<option value="${escapeHtml(folder.uri)}">${escapeHtml(folder.name)} (${escapeHtml(folder.uri)})</option>` | |
| ) | |
| .join("") | |
| : `<option value="">No folders in this workspace</option>`; | |
| return ` | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta | |
| http-equiv="Content-Security-Policy" | |
| content="default-src 'none'; img-src vscode-resource: https:; script-src 'unsafe-inline'; style-src 'unsafe-inline';" | |
| /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Harness Swap Wizard</title> | |
| <style> | |
| :root { color-scheme: light dark; font-family: var(--vscode-font-family, sans-serif); } | |
| body { margin: 12px; font-size: 13px; line-height: 1.4; } | |
| .row { margin-bottom: 12px; } | |
| .grid { display: grid; gap: 8px; } | |
| label { display: block; margin-bottom: 4px; } | |
| button { cursor: pointer; } | |
| input, select { width: 100%; box-sizing: border-box; } | |
| .log { margin-top: 12px; border: 1px solid var(--vscode-panel-border); padding: 8px; height: 260px; overflow: auto; white-space: pre-wrap; } | |
| .line { margin-bottom: 2px; } | |
| .ok { color: var(--vscode-terminal-ansiGreen); } | |
| .warn { color: var(--vscode-terminal-ansiYellow); } | |
| .error { color: var(--vscode-terminal-ansiRed); } | |
| .debug { color: var(--vscode-terminal-ansiBlue); } | |
| .header { margin-bottom: 8px; font-weight: 600; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header">Harness Swap Migration Wizard</div> | |
| <div class="grid"> | |
| <div class="row"> | |
| <label for="source-root">Source workspace folder</label> | |
| <select id="source-root" ${hasWorkspaceFolders ? "" : "disabled"}> | |
| ${optionsMarkup} | |
| </select> | |
| </div> | |
| <div class="row"> | |
| <label for="output-root">Output root (optional)</label> | |
| <input id="output-root" type="text" placeholder="Defaults to <source>/.harness-swap" /> | |
| </div> | |
| <div class="row"> | |
| <label for="package-name">Package file</label> | |
| <input id="package-name" type="text" placeholder="Defaults to harness-swap-<source>-<timestamp>.zip" /> | |
| </div> | |
| <div class="row"> | |
| <label for="max-file-bytes">Max file bytes</label> | |
| <input id="max-file-bytes" type="number" min="1" max="10485760" value="524288" /> | |
| </div> | |
| <div class="row"> | |
| <label for="target-model">Target model (optional)</label> | |
| <input id="target-model" type="text" placeholder="gpt-5.4" /> | |
| </div> | |
| <div class="row"> | |
| <label><input id="strict" type="checkbox" /> Strict mode</label> | |
| </div> | |
| <div class="row"> | |
| <label><input id="include-evidence" type="checkbox" checked /> Include evidence</label> | |
| </div> | |
| <div class="row"> | |
| <button id="run">Run migration</button> | |
| <button id="stop" type="button">Stop</button> | |
| </div> | |
| <div id="status" class="row"></div> | |
| <div id="log" class="log" aria-live="polite"></div> | |
| </div> | |
| <script> | |
| const hasSourceFolder = ${hasSourceOption}; | |
| const vscode = acquireVsCodeApi(); | |
| const logEl = document.getElementById("log"); | |
| const statusEl = document.getElementById("status"); | |
| const sourceRootEl = document.getElementById("source-root"); | |
| const outputRootEl = document.getElementById("output-root"); | |
| const packageNameEl = document.getElementById("package-name"); | |
| const maxFileBytesEl = document.getElementById("max-file-bytes"); | |
| const targetModelEl = document.getElementById("target-model"); | |
| const strictEl = document.getElementById("strict"); | |
| const evidenceEl = document.getElementById("include-evidence"); | |
| const runEl = document.getElementById("run"); | |
| const stopEl = document.getElementById("stop"); | |
| const write = (line, cls = "") => { | |
| const node = document.createElement("div"); | |
| node.className = "line " + cls; | |
| node.textContent = line; | |
| logEl.appendChild(node); | |
| logEl.scrollTop = logEl.scrollHeight; | |
| }; | |
| if (!hasSourceFolder) { | |
| runEl.disabled = true; | |
| statusEl.textContent = "No workspace folders available. Open a folder and try again."; | |
| } | |
| runEl.addEventListener("click", () => { | |
| const maxFileBytes = Number.parseInt(maxFileBytesEl.value, 10); | |
| vscode.postMessage({ | |
| type: "wizard.run.start", | |
| sourceRoot: sourceRootEl.value, | |
| outputRoot: outputRootEl.value || undefined, | |
| packageName: packageNameEl.value || undefined, | |
| options: { | |
| strict: strictEl.checked, | |
| includeEvidence: evidenceEl.checked, | |
| targetModel: targetModelEl.value || undefined, | |
| maxFileBytes: Number.isFinite(maxFileBytes) && maxFileBytes > 0 ? maxFileBytes : undefined | |
| } | |
| }); | |
| statusEl.textContent = "Run requested..."; | |
| }); | |
| stopEl.addEventListener("click", () => { | |
| vscode.postMessage({ type: "wizard.run.stop" }); | |
| statusEl.textContent = "Stop requested."; | |
| }); | |
| window.addEventListener("message", (event) => { | |
| const msg = event.data; | |
| switch (msg.type) { | |
| case "wizard.log": | |
| write("[" + msg.level + "] " + msg.message, msg.level === "error" ? "error" : msg.level === "warning" ? "warn" : msg.level === "debug" ? "debug" : "ok"); | |
| break; | |
| case "wizard.progress": | |
| statusEl.textContent = msg.phase + ": " + msg.detail; | |
| write("STATUS: " + msg.phase + ": " + msg.detail); | |
| break; | |
| case "wizard.result": | |
| statusEl.textContent = "Done: " + msg.fileCount + " files exported to " + msg.outputFolder; | |
| write("Done: " + msg.fileCount + " files exported to " + msg.outputFolder); | |
| break; | |
| case "wizard.error": | |
| statusEl.textContent = "Error: " + msg.message; | |
| write("ERROR: " + msg.message, "error"); | |
| break; | |
| } | |
| }); | |
| vscode.postMessage({ type: "wizard.ready" }); | |
| </script> | |
| </body> | |
| </html> | |
| `; | |
| } | |
| } | |
| function isDescendant(basePath: string, candidatePath: string): boolean { | |
| const normalizedBase = `${path.resolve(basePath)}${path.sep}`; | |
| return candidatePath.startsWith(normalizedBase) && path.resolve(candidatePath) !== path.resolve(basePath); | |
| } | |
| function ensureZipExtension(value: string): string { | |
| return value.toLowerCase().endsWith(".zip") ? value : `${value}.zip`; | |
| } | |
| function escapeHtml(text: string): string { | |
| return text | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """); | |
| } | |