From 1e959d7cdb4d95b7520a040de068e1b60e942245 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:25:28 +0000 Subject: [PATCH 1/2] Initial plan From 19589c10a128424c8f2752f63ea10eb1d8f97847 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:31:15 +0000 Subject: [PATCH 2/2] fix: handle SCAFFOLD_MANIFEST.yml (underscore) in downloaded tarballs Agent-Logs-Url: https://github.com/paralleldrive/aidd/sessions/2a1061da-4769-46c5-b1f3-4aef438d61e9 --- lib/scaffold-create.js | 2 +- lib/scaffold-create.test.js | 33 ++++++++++++++++ lib/scaffold-resolver.js | 30 ++++++++++++++- lib/scaffold-resolver.test.js | 72 +++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/lib/scaffold-create.js b/lib/scaffold-create.js index 65decf3d..3e28e1f2 100644 --- a/lib/scaffold-create.js +++ b/lib/scaffold-create.js @@ -90,7 +90,7 @@ const runCreate = async ({ await runManifestFn({ agentConfig, folder, - manifestPath: path.join(folder, "SCAFFOLD-MANIFEST.yml"), + manifestPath: path.join(folder, path.basename(paths.manifestPath)), }); if (prompt != null) { diff --git a/lib/scaffold-create.test.js b/lib/scaffold-create.test.js index fd2e5586..0c0faba3 100644 --- a/lib/scaffold-create.test.js +++ b/lib/scaffold-create.test.js @@ -330,6 +330,39 @@ describe("runCreate", () => { }); }); + test("uses SCAFFOLD_MANIFEST.yml (underscore) manifest filename when resolver returns it", async () => { + // Simulates a GitHub tarball where the manifest file has underscores instead of hyphens + const underscoreScaffoldDir = path.join(tempDir, "underscore-scaffold"); + await fs.ensureDir(underscoreScaffoldDir); + await fs.writeFile( + path.join(underscoreScaffoldDir, "SCAFFOLD_MANIFEST.yml"), + "steps: []\n", + ); + + const calls = /** @type {Array<{manifestPath?: string}>} */ ([]); + const trackingManifest = async ( + /** @type {{manifestPath?: string}} */ { manifestPath }, + ) => { + calls.push({ manifestPath }); + }; + + await runCreate({ + folder: projectDir, + resolveExtensionFn: async () => ({ + manifestPath: path.join(underscoreScaffoldDir, "SCAFFOLD_MANIFEST.yml"), + downloaded: false, + }), + runManifestFn: trackingManifest, + }); + + assert({ + given: "resolver returns a SCAFFOLD_MANIFEST.yml (underscore) path", + should: "pass the underscore manifest path to runManifest", + actual: calls[0]?.manifestPath, + expected: path.join(projectDir, "SCAFFOLD_MANIFEST.yml"), + }); + }); + test("given --prompt with echo agent, invokes agent and returns success", async () => { const result = await runCreate({ folder: projectDir, diff --git a/lib/scaffold-resolver.js b/lib/scaffold-resolver.js index 522646e9..c1a616c1 100644 --- a/lib/scaffold-resolver.js +++ b/lib/scaffold-resolver.js @@ -251,6 +251,32 @@ const resolveFileUri = ({ uri }) => { const SCAFFOLD_DOWNLOAD_DIR = path.join(AIDD_HOME, "scaffold"); +// GitHub release tarballs may convert hyphens to underscores in filenames, +// so SCAFFOLD-MANIFEST.yml can arrive as SCAFFOLD_MANIFEST.yml. +// This list is checked in priority order: the hyphen form is canonical. +const SCAFFOLD_MANIFEST_NAMES = [ + "SCAFFOLD-MANIFEST.yml", + "SCAFFOLD_MANIFEST.yml", +]; + +/** + * Resolves the manifest path within a directory, checking both the canonical + * hyphen form and the underscore form that GitHub tarballs may produce. + * Returns the path to the first existing variant, or the canonical path as a + * fallback (so callers receive a meaningful path in error messages). + * @param {string} dir + */ +const resolveManifestPath = async (dir) => { + for (const name of SCAFFOLD_MANIFEST_NAMES) { + const manifestPath = path.join(dir, name); + if (await fs.pathExists(manifestPath)) { + return manifestPath; + } + } + // Fallback: return canonical name so error messages are informative + return path.join(dir, SCAFFOLD_MANIFEST_NAMES[0]); +}; + /** * @param {{uri: string, scaffoldDownloadDir?: string, download: Function}} options */ @@ -268,7 +294,7 @@ const downloadExtension = async ({ await fs.ensureDir(scaffoldDownloadDir); await download(uri, scaffoldDownloadDir); return { - manifestPath: path.join(scaffoldDownloadDir, "SCAFFOLD-MANIFEST.yml"), + manifestPath: await resolveManifestPath(scaffoldDownloadDir), readmePath: path.join(scaffoldDownloadDir, "README.md"), }; }; @@ -417,5 +443,7 @@ export { defaultDownloadAndExtract, defaultResolveRelease, resolveExtension, + resolveManifestPath, SCAFFOLD_DOWNLOAD_DIR, + SCAFFOLD_MANIFEST_NAMES, }; diff --git a/lib/scaffold-resolver.test.js b/lib/scaffold-resolver.test.js index 03a40a74..c6654625 100644 --- a/lib/scaffold-resolver.test.js +++ b/lib/scaffold-resolver.test.js @@ -1041,6 +1041,78 @@ describe("resolveExtension - manifest existence validation", () => { }); }); +describe("resolveExtension - SCAFFOLD_MANIFEST.yml (underscore) fallback", () => { + /** @type {string} */ + let tempDir; + + beforeEach(async () => { + tempDir = path.join(os.tmpdir(), `aidd-underscore-manifest-${Date.now()}`); + await fs.ensureDir(tempDir); + }); + + afterEach(async () => { + await fs.remove(tempDir); + }); + + test("resolves SCAFFOLD_MANIFEST.yml when SCAFFOLD-MANIFEST.yml is absent (HTTP download)", async () => { + // Simulates a GitHub tarball that produced SCAFFOLD_MANIFEST.yml instead of SCAFFOLD-MANIFEST.yml + // @ts-expect-error + const downloadWithUnderscore = async (_url, destPath) => { + await fs.ensureDir(destPath); + await fs.writeFile( + path.join(destPath, "SCAFFOLD_MANIFEST.yml"), + "steps:\n - run: echo hello\n", + ); + }; + + const paths = await resolveExtension({ + type: "https://example.com/scaffold.tar.gz", + scaffoldDownloadDir: path.join(tempDir, "scaffold"), + confirm: noConfirm, + download: downloadWithUnderscore, + log: noLog, + }); + + assert({ + given: "a downloaded scaffold with SCAFFOLD_MANIFEST.yml (underscore)", + should: "resolve without error and return the underscore manifest path", + actual: paths.manifestPath.endsWith("SCAFFOLD_MANIFEST.yml"), + expected: true, + }); + }); + + test("prefers SCAFFOLD-MANIFEST.yml (hyphen) over SCAFFOLD_MANIFEST.yml (underscore) when both exist", async () => { + // @ts-expect-error + const downloadWithBoth = async (_url, destPath) => { + await fs.ensureDir(destPath); + await fs.writeFile( + path.join(destPath, "SCAFFOLD-MANIFEST.yml"), + "steps:\n - run: echo hyphen\n", + ); + await fs.writeFile( + path.join(destPath, "SCAFFOLD_MANIFEST.yml"), + "steps:\n - run: echo underscore\n", + ); + }; + + const paths = await resolveExtension({ + type: "https://example.com/scaffold.tar.gz", + scaffoldDownloadDir: path.join(tempDir, "scaffold"), + confirm: noConfirm, + download: downloadWithBoth, + log: noLog, + }); + + assert({ + given: + "a downloaded scaffold with both SCAFFOLD-MANIFEST.yml and SCAFFOLD_MANIFEST.yml", + should: "prefer the canonical hyphen form", + actual: paths.manifestPath.endsWith("SCAFFOLD-MANIFEST.yml"), + expected: true, + }); + }); +}); + describe("defaultResolveRelease - GITHUB_TOKEN auth and error messages", () => { /** @type {any} */ let originalFetch;