diff --git a/apps/daemon/src/artifact-manifest.ts b/apps/daemon/src/artifact-manifest.ts
index ead9b55e0..49b8c6f1c 100644
--- a/apps/daemon/src/artifact-manifest.ts
+++ b/apps/daemon/src/artifact-manifest.ts
@@ -201,10 +201,15 @@ export function validateArtifactManifestInput(
}
}
- const safeEntry = typeof entry === 'string' ? entry : '';
- if (!safeEntry || safeEntry.length > MAX_ENTRY_LENGTH) {
- return { ok: false, error: `artifact entry exceeds max length (${MAX_ENTRY_LENGTH})` };
+ const manifestEntry =
+ typeof manifest.entry === 'string' && manifest.entry.trim()
+ ? manifest.entry.trim()
+ : entry;
+ const entryErr = validateSupportingPath(manifestEntry);
+ if (entryErr) {
+ return { ok: false, error: `artifactManifest.entry ${entryErr}` };
}
+ const safeEntry = (manifestEntry as string).replace(/\\/g, '/');
return { ok: true, value: sanitizeManifest(manifest, safeEntry, options) };
}
diff --git a/apps/daemon/src/import-export-routes.ts b/apps/daemon/src/import-export-routes.ts
index a6489e5f8..fbd06bbff 100644
--- a/apps/daemon/src/import-export-routes.ts
+++ b/apps/daemon/src/import-export-routes.ts
@@ -731,7 +731,7 @@ function buildProjectExportManifestResponse({
artifactSupporting.add(normalized);
note(normalized, reason);
};
- addManifestRef(manifest.entry, 'artifact-entry');
+ addManifestRef(manifest.entry, 'artifact-entry', { preferProjectRoot: true });
if (typeof manifest.primary === 'string') {
addManifestRef(manifest.primary, 'artifact-primary', { preferProjectRoot: true });
}
@@ -798,12 +798,18 @@ function chooseExportManifestEntryFile(
for (const file of files) {
const manifest = file.artifactManifest;
if (!manifest || typeof manifest !== 'object') continue;
+ if (isInferredArtifactManifest(manifest)) continue;
if (manifest.primary === true) return file.name;
- if (typeof manifest.primary !== 'string') continue;
- const rootPrimary = normalizeManifestProjectRootRef(manifest.primary);
- if (rootPrimary && filesByName.has(rootPrimary)) return rootPrimary;
- const ownerRelativePrimary = normalizeManifestProjectRef(manifest.primary, file.name);
- if (ownerRelativePrimary && filesByName.has(ownerRelativePrimary)) return ownerRelativePrimary;
+ if (typeof manifest.primary === 'string') {
+ const rootPrimary = normalizeManifestProjectRootRef(manifest.primary);
+ if (rootPrimary && filesByName.has(rootPrimary)) return rootPrimary;
+ const ownerRelativePrimary = normalizeManifestProjectRef(manifest.primary, file.name);
+ if (ownerRelativePrimary && filesByName.has(ownerRelativePrimary)) return ownerRelativePrimary;
+ }
+ const rootEntry = normalizeManifestProjectRootRef(manifest.entry);
+ if (rootEntry && filesByName.has(rootEntry)) return rootEntry;
+ const ownerRelativeEntry = normalizeManifestProjectRef(manifest.entry, file.name);
+ if (ownerRelativeEntry && filesByName.has(ownerRelativeEntry)) return ownerRelativeEntry;
}
return files.find((file) => /(^|\/)index\.html?$/i.test(file.name))?.name
?? files.find((file) => file.kind === 'html')?.name
diff --git a/apps/daemon/tests/export-manifest-route.test.ts b/apps/daemon/tests/export-manifest-route.test.ts
index edea6b60c..2ae5a924c 100644
--- a/apps/daemon/tests/export-manifest-route.test.ts
+++ b/apps/daemon/tests/export-manifest-route.test.ts
@@ -176,6 +176,44 @@ describe('project export manifest route', () => {
});
});
+ it('uses artifact entry strings as project-relative entry refs without primary hints', async () => {
+ const projectId = await createProject({ kind: 'prototype' });
+ await writeFile(projectId, {
+ name: 'index.html',
+ content: 'fallback',
+ });
+ await writeFile(projectId, {
+ name: 'reviewed.html',
+ content: 'reviewed',
+ });
+ await writeFile(projectId, {
+ name: 'preview/wrapper.html',
+ content: '',
+ artifactManifest: {
+ version: 1,
+ kind: 'html',
+ title: 'Review wrapper',
+ entry: 'reviewed.html',
+ renderer: 'html',
+ status: 'complete',
+ exports: ['html'],
+ },
+ });
+
+ const response = await fetch(`${baseUrl}/api/projects/${projectId}/export/manifest`);
+ expect(response.ok).toBe(true);
+ const body = await response.json() as {
+ entryFile: string;
+ files: Array<{ name: string; role: string; reasons: string[] }>;
+ };
+
+ expect(body.entryFile).toBe('reviewed.html');
+ expect(body.files.find((file) => file.name === 'reviewed.html')).toMatchObject({
+ role: 'entry',
+ reasons: expect.arrayContaining(['artifact-entry', 'project-entry-file']),
+ });
+ });
+
it('rejects invalid project ids before listing files', async () => {
const response = await fetch(`${baseUrl}/api/projects/bad:id/export/manifest`);
expect(response.status).toBe(400);