@resciencelab/shader-cli
Advanced tools
+39
-64
@@ -12,2 +12,7 @@ import { success, error } from "../utils/output.js"; | ||
| } | ||
| function buildRuntimeUrl(baseUrl, projectJson) { | ||
| const encoded = Buffer.from(projectJson).toString("base64"); | ||
| const sep = baseUrl.includes("?") ? "&" : "?"; | ||
| return `${baseUrl}${sep}project=${encodeURIComponent(encoded)}&autoplay=true`; | ||
| } | ||
| async function loadPlaywright() { | ||
@@ -32,2 +37,3 @@ try { | ||
| headless: false, | ||
| channel: "chrome", | ||
| args: [ | ||
@@ -37,4 +43,2 @@ "--enable-unsafe-webgpu", | ||
| "--use-angle=metal", | ||
| "--window-position=-2400,-2400", | ||
| "--window-size=1,1", | ||
| ], | ||
@@ -44,4 +48,9 @@ }); | ||
| try { | ||
| const context = await browser.newContext({ acceptDownloads: true }); | ||
| const context = await browser.newContext({ | ||
| acceptDownloads: true, | ||
| viewport: { width: 1920, height: 1080 }, | ||
| }); | ||
| const page = await context.newPage(); | ||
| const project = JSON.parse(projectJson); | ||
| const layerCount = project.layers?.length ?? 0; | ||
| let hostname; | ||
@@ -54,48 +63,19 @@ try { | ||
| } | ||
| const sp3 = new Spinner(`Loading ${hostname}`).spin(); | ||
| await page.goto(runtimeUrl, { waitUntil: "networkidle" }); | ||
| await page.waitForTimeout(3000); | ||
| sp3.succeed("Runtime loaded"); | ||
| const project = JSON.parse(projectJson); | ||
| const layerCount = project.layers?.length ?? 0; | ||
| const sp4 = new Spinner(`Injecting project (${layerCount} layers, ${opts.duration}s)`).spin(); | ||
| await page.evaluate((json) => { | ||
| const projectFile = JSON.parse(json); | ||
| const { useLayerStore, useTimelineStore, useEditorStore } = window.__SHADER_LAB_STORES__ ?? {}; | ||
| if (useLayerStore) { | ||
| useLayerStore.getState().replaceState(projectFile.layers, projectFile.selectedLayerId, null); | ||
| } | ||
| if (useTimelineStore) { | ||
| useTimelineStore.getState().replaceState({ | ||
| currentTime: 0, | ||
| duration: projectFile.timeline.duration, | ||
| isPlaying: true, | ||
| loop: projectFile.timeline.loop, | ||
| selectedKeyframeId: null, | ||
| selectedTrackId: null, | ||
| tracks: projectFile.timeline.tracks, | ||
| }); | ||
| } | ||
| if (useEditorStore && projectFile.sceneConfig) { | ||
| useEditorStore.getState().updateSceneConfig(projectFile.sceneConfig); | ||
| useEditorStore.getState().setOutputSize(projectFile.composition.width, projectFile.composition.height); | ||
| } | ||
| }, projectJson); | ||
| await page.waitForTimeout(2000); | ||
| sp4.succeed("Project injected"); | ||
| const sp3 = new Spinner(`Loading ${hostname} (${layerCount} layers)`).spin(); | ||
| const fullUrl = buildRuntimeUrl(runtimeUrl, projectJson); | ||
| await page.goto(fullUrl, { waitUntil: "networkidle" }); | ||
| // Wait for URL params loader to apply project (1.5s delay in component + render time) | ||
| await page.waitForTimeout(5000); | ||
| sp3.succeed("Runtime loaded & project injected"); | ||
| if (opts.format === "png") { | ||
| const sp5 = new Spinner("Exporting PNG").spin(); | ||
| // Open export dialog using Playwright locator | ||
| await page.locator('button[aria-label="Export"]').first().click(); | ||
| await page.waitForTimeout(1500); | ||
| const downloadPromise = page.waitForEvent("download", { timeout: 30000 }); | ||
| await page.evaluate((_time) => { | ||
| const exportBtn = document.querySelector('button[aria-label="Export"]'); | ||
| exportBtn?.click(); | ||
| setTimeout(() => { | ||
| const imgTab = [...document.querySelectorAll("button")].find(b => b.textContent?.trim() === "image"); | ||
| imgTab?.click(); | ||
| setTimeout(() => { | ||
| const exportPng = [...document.querySelectorAll("button")].find(b => b.textContent?.includes("Export PNG")); | ||
| exportPng?.click(); | ||
| }, 500); | ||
| }, 1000); | ||
| }, opts.time); | ||
| // Click image tab (default, but ensure) | ||
| await page.getByRole("button", { name: "image", exact: true }).click(); | ||
| await page.waitForTimeout(500); | ||
| // Click Export PNG | ||
| await page.getByRole("button", { name: "Export PNG" }).click(); | ||
| const download = await downloadPromise; | ||
@@ -112,21 +92,16 @@ sp5.succeed("PNG captured"); | ||
| const spRec = new Spinner(`Recording ${formatLabel} (${opts.duration}s @ ${opts.fps}fps)`).spin(); | ||
| // Open export dialog | ||
| await page.locator('button[aria-label="Export"]').first().click(); | ||
| await page.waitForTimeout(1500); | ||
| const downloadPromise = page.waitForEvent("download", { timeout: 120000 }); | ||
| await page.evaluate((format) => { | ||
| const exportBtn = document.querySelector('button[aria-label="Export"]'); | ||
| exportBtn?.click(); | ||
| setTimeout(() => { | ||
| const videoTab = [...document.querySelectorAll("button")].find(b => b.textContent?.trim() === "video"); | ||
| videoTab?.click(); | ||
| setTimeout(() => { | ||
| if (format === "mp4") { | ||
| const mp4Btn = [...document.querySelectorAll("button")].find(b => b.textContent?.trim() === "MP4"); | ||
| mp4Btn?.click(); | ||
| } | ||
| setTimeout(() => { | ||
| const btn = [...document.querySelectorAll("button")].find(b => b.textContent?.includes("Export WEBM") || b.textContent?.includes("Export MP4")); | ||
| btn?.click(); | ||
| }, 500); | ||
| }, 500); | ||
| }, 1000); | ||
| }, opts.format); | ||
| // Click video tab | ||
| await page.getByRole("button", { name: "video", exact: true }).click(); | ||
| await page.waitForTimeout(500); | ||
| // Select format if MP4 | ||
| if (opts.format === "mp4") { | ||
| await page.getByRole("button", { name: "MP4", exact: true }).click(); | ||
| await page.waitForTimeout(300); | ||
| } | ||
| // Click Export WEBM / Export MP4 | ||
| await page.getByRole("button", { name: `Export ${formatLabel}` }).click(); | ||
| const recStart = Date.now(); | ||
@@ -133,0 +108,0 @@ const tick = setInterval(() => pb.update(Date.now() - recStart), 200); |
+1
-1
@@ -18,3 +18,3 @@ #!/usr/bin/env node | ||
| .description("Agent-native CLI for Shader Lab — compose and export WebGPU shader scenes from the terminal") | ||
| .version("0.1.1") | ||
| .version("0.1.2") | ||
| .option("--json", "Output as JSON (agent-friendly)") | ||
@@ -21,0 +21,0 @@ .option("--project <path>", "Open a .lab project file") |
+1
-1
| { | ||
| "name": "@resciencelab/shader-cli", | ||
| "version": "0.1.1", | ||
| "version": "0.1.2", | ||
| "description": "Agent-native CLI for Shader Lab — compose and export WebGPU shader scenes from the terminal", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
2
-33.33%84245
-1.61%1806
-1.37%