diff --git a/scripts/build.js b/scripts/build.js index 6e32dd0f..eb48b7c1 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -122,6 +122,12 @@ async function main() { version: BUILD_VERSION, }); console.log(` ✓ ${agentsResult.count} agent prompt(s) → dist/agents/ + agent-menu.json`); + if (agentsResult.baseUrl.startsWith('file:')) { + console.log( + ' ! Unversioned build: prompts point at the local files, not at a release.\n' + + ' Set AGENTS_BASE_URL, or run `npm run dev`, to serve them over HTTP.', + ); + } const releaseAssetDocs = docEntries.filter(d => d.release_asset); if (releaseAssetDocs.length > 0) { diff --git a/scripts/lib/agent-generator.js b/scripts/lib/agent-generator.js index 9e6aea3d..8cc70f43 100644 --- a/scripts/lib/agent-generator.js +++ b/scripts/lib/agent-generator.js @@ -14,13 +14,23 @@ import fs from 'fs'; import path from 'path'; +import { pathToFileURL } from 'url'; import { REPO_URL } from './constants.js'; -/** Release assets are a flat namespace: agent prompts live at the release root, like skills. */ -function defaultAgentsBaseUrl(version) { +/** + * Where a build says its prompts live. + * + * Release assets are a flat namespace: agent prompts live at the release root, + * like skills. An unversioned build has no release, so it points at the files + * it just wrote: `releases/latest/download` made it advertise prompts the + * latest release does not carry, and a flow still on a branch then failed the + * whole run with a bare 404. Set `AGENTS_BASE_URL`, as the dev server does, to + * serve the same files over HTTP. + */ +function defaultAgentsBaseUrl(version, agentsDistDir) { return version && version !== 'dev' ? `${REPO_URL}/releases/download/v${version}` - : `${REPO_URL}/releases/latest/download`; + : pathToFileURL(agentsDistDir).href; } /** @@ -98,12 +108,13 @@ function assertRunnerSeededHasSink(flows) { * Copy every agent-prompt markdown file into dist/agents// and write the * menu the wizard fetches to discover available types. Each menu entry carries * its flow and a full downloadUrl so the dev-server and the release host can - * differ without the wizard composing URLs. Returns { count, agentsDistDir }. + * differ without the wizard composing URLs. Returns + * { count, agentsDistDir, baseUrl }. */ export function buildAgents({ configDir, distDir, baseUrl, version = 'dev' }) { const agentsSourceDir = path.join(configDir, 'agents'); const agentsDistDir = path.join(distDir, 'agents'); - const resolvedBase = (baseUrl || defaultAgentsBaseUrl(version)).replace(/\/+$/, ''); + const resolvedBase = (baseUrl || defaultAgentsBaseUrl(version, agentsDistDir)).replace(/\/+$/, ''); fs.mkdirSync(agentsDistDir, { recursive: true }); @@ -147,5 +158,5 @@ export function buildAgents({ configDir, distDir, baseUrl, version = 'dev' }) { }; walk(agentsDistDir); - return { count: agents.length, agentsDistDir }; + return { count: agents.length, agentsDistDir, baseUrl: resolvedBase }; } diff --git a/scripts/lib/tests/agent-generator-flow.test.js b/scripts/lib/tests/agent-generator-flow.test.js index 44cb2843..2b494a33 100644 --- a/scripts/lib/tests/agent-generator-flow.test.js +++ b/scripts/lib/tests/agent-generator-flow.test.js @@ -1,9 +1,11 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdirSync, writeFileSync, mkdtempSync, rmSync, readFileSync, existsSync } from 'fs'; import { join } from 'path'; +import { pathToFileURL, fileURLToPath } from 'url'; import { tmpdir } from 'os'; import { buildAgents } from '../agent-generator.js'; +import { REPO_URL } from '../constants.js'; const prompt = (frontmatter) => `---\n${frontmatter}\n---\n\n## Goal\n\nDo the thing.\n`; @@ -100,3 +102,40 @@ describe('buildAgents flow frontmatter', () => { expect(existsSync(join(distDir, 'agents', 'my-flow', 'README.md'))).toBe(false); }); }); + +describe('buildAgents download URLs', () => { + let tmpDir; + let configDir; + let distDir; + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), 'agents-url-test-')); + configDir = join(tmpDir, 'context'); + distDir = join(tmpDir, 'dist'); + mkdirSync(join(configDir, 'agents', 'my-flow'), { recursive: true }); + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task\nflow: my-flow'), + ); + }); + + afterEach(() => rmSync(tmpDir, { recursive: true, force: true })); + + const menuOf = () => + JSON.parse(readFileSync(join(distDir, 'agents', 'agent-menu.json'), 'utf8')); + + it('pins a versioned build to its own release', () => { + buildAgents({ configDir, distDir, version: '1.47.0' }); + expect(menuOf().agents[0].downloadUrl).toBe( + `${REPO_URL}/releases/download/v1.47.0/agents-my-flow-task.md`, + ); + }); + + it('points an unversioned build at the files it just wrote', () => { + const { baseUrl } = buildAgents({ configDir, distDir }); + const url = menuOf().agents[0].downloadUrl; + expect(baseUrl).toBe(pathToFileURL(join(distDir, 'agents')).href); + expect(url).toBe(pathToFileURL(join(distDir, 'agents', 'agents-my-flow-task.md')).href); + expect(existsSync(fileURLToPath(url))).toBe(true); + }); +});