Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/scaffold-create.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
33 changes: 33 additions & 0 deletions lib/scaffold-create.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 29 additions & 1 deletion lib/scaffold-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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"),
};
};
Expand Down Expand Up @@ -417,5 +443,7 @@ export {
defaultDownloadAndExtract,
defaultResolveRelease,
resolveExtension,
resolveManifestPath,
SCAFFOLD_DOWNLOAD_DIR,
SCAFFOLD_MANIFEST_NAMES,
};
72 changes: 72 additions & 0 deletions lib/scaffold-resolver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down