mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(cli): add --public flag to publish (#1815)
Opt-in --public flag on `hyperframes publish` sends is_public to the publish endpoints (staged complete body and direct multipart form) so a claimed project's studio session can be created public instead of the default private. Absent the flag the request shape is unchanged.
This commit is contained in:
@@ -13,6 +13,7 @@ import { publishProjectArchive } from "../utils/publishProject.js";
|
||||
export const examples: Example[] = [
|
||||
["Publish the current project with a public URL", "hyperframes publish"],
|
||||
["Publish a specific directory", "hyperframes publish ./my-video"],
|
||||
["Make the claimed project public to anyone", "hyperframes publish --public"],
|
||||
["Skip the consent prompt (scripts)", "hyperframes publish --yes"],
|
||||
];
|
||||
|
||||
@@ -29,6 +30,11 @@ export default defineCommand({
|
||||
description: "Skip the publish confirmation prompt",
|
||||
default: false,
|
||||
},
|
||||
public: {
|
||||
type: "boolean",
|
||||
description: "Make the claimed project public to anyone, not just the claimer",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
const rawArg = args.dir;
|
||||
@@ -66,7 +72,7 @@ export default defineCommand({
|
||||
publishSpinner.start("Uploading project...");
|
||||
|
||||
try {
|
||||
const published = await publishProjectArchive(dir);
|
||||
const published = await publishProjectArchive(dir, { public: args.public === true });
|
||||
const claimUrl = new URL(published.url);
|
||||
claimUrl.searchParams.set("claim_token", published.claimToken);
|
||||
publishSpinner.stop(c.success("Project published"));
|
||||
|
||||
@@ -453,6 +453,99 @@ describe("publishProjectArchive", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("sends is_public in the staged complete body only when public is requested", async () => {
|
||||
const dir = makeProjectDir();
|
||||
const stagedFetch = () =>
|
||||
vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
upload_url: "https://s3.example.com/upload",
|
||||
upload_key: "ephemeral_store/hyperframes/project_uploads/upload-1/demo.zip",
|
||||
upload_headers: { "content-type": "application/zip" },
|
||||
content_type: "application/zip",
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
)
|
||||
.mockResolvedValueOnce(new Response(null, { status: 200 }))
|
||||
.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
project_id: "hfp_123",
|
||||
title: "demo",
|
||||
file_count: 1,
|
||||
url: "https://hyperframes.dev/p/hfp_123",
|
||||
claim_token: "claim-token",
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
writeFileSync(join(dir, "index.html"), "<html></html>", "utf-8");
|
||||
|
||||
const publicFetch = stagedFetch();
|
||||
vi.stubGlobal("fetch", publicFetch);
|
||||
await publishProjectArchive(dir, { public: true });
|
||||
const publicCompleteBody = JSON.parse(publicFetch.mock.calls[2]![1].body);
|
||||
expect(publicCompleteBody.is_public).toBe(true);
|
||||
|
||||
const defaultFetch = stagedFetch();
|
||||
vi.stubGlobal("fetch", defaultFetch);
|
||||
await publishProjectArchive(dir);
|
||||
const defaultCompleteBody = JSON.parse(defaultFetch.mock.calls[2]![1].body);
|
||||
expect(defaultCompleteBody).not.toHaveProperty("is_public");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("sends is_public in the direct multipart form only when public is requested", async () => {
|
||||
const dir = makeProjectDir();
|
||||
const directFetch = () =>
|
||||
vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(new Response("not found", { status: 404 }))
|
||||
.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
project_id: "hfp_123",
|
||||
title: "demo",
|
||||
file_count: 1,
|
||||
url: "https://hyperframes.dev/p/hfp_123",
|
||||
claim_token: "claim-token",
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
writeFileSync(join(dir, "index.html"), "<html></html>", "utf-8");
|
||||
|
||||
const publicFetch = directFetch();
|
||||
vi.stubGlobal("fetch", publicFetch);
|
||||
await publishProjectArchive(dir, { public: true });
|
||||
const publicForm = publicFetch.mock.calls[1]![1].body as FormData;
|
||||
expect(publicForm.get("is_public")).toBe("true");
|
||||
|
||||
const defaultFetch = directFetch();
|
||||
vi.stubGlobal("fetch", defaultFetch);
|
||||
await publishProjectArchive(dir);
|
||||
const defaultForm = defaultFetch.mock.calls[1]![1].body as FormData;
|
||||
expect(defaultForm.get("is_public")).toBeNull();
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not fall back to multipart when a staged S3 upload fails", async () => {
|
||||
const dir = makeProjectDir();
|
||||
const fetchMock = vi
|
||||
|
||||
@@ -383,9 +383,11 @@ async function publishProjectArchiveDirect(
|
||||
apiBaseUrl: string,
|
||||
title: string,
|
||||
archive: PublishArchiveResult,
|
||||
isPublic: boolean,
|
||||
): Promise<PublishedProjectResponse> {
|
||||
const body = new FormData();
|
||||
body.set("title", title);
|
||||
if (isPublic) body.set("is_public", "true");
|
||||
body.set(
|
||||
"file",
|
||||
new File([archiveArrayBuffer(archive)], `${title}.zip`, { type: PUBLISH_CONTENT_TYPE }),
|
||||
@@ -414,6 +416,7 @@ async function publishProjectArchiveStaged(
|
||||
apiBaseUrl: string,
|
||||
title: string,
|
||||
archive: PublishArchiveResult,
|
||||
isPublic: boolean,
|
||||
): Promise<PublishedProjectResponse | null> {
|
||||
const fileName = `${title}.zip`;
|
||||
const uploadResponse = await fetch(`${apiBaseUrl}/v1/hyperframes/projects/publish/upload`, {
|
||||
@@ -459,6 +462,7 @@ async function publishProjectArchiveStaged(
|
||||
upload_key: stagedUpload.uploadKey,
|
||||
file_name: fileName,
|
||||
title,
|
||||
...(isPublic ? { is_public: true } : {}),
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
@@ -476,11 +480,19 @@ async function publishProjectArchiveStaged(
|
||||
return publishedProject;
|
||||
}
|
||||
|
||||
export async function publishProjectArchive(projectDir: string): Promise<PublishedProjectResponse> {
|
||||
export interface PublishOptions {
|
||||
public?: boolean;
|
||||
}
|
||||
|
||||
export async function publishProjectArchive(
|
||||
projectDir: string,
|
||||
opts: PublishOptions = {},
|
||||
): Promise<PublishedProjectResponse> {
|
||||
const isPublic = opts.public === true;
|
||||
const title = basename(projectDir);
|
||||
const archive = createPublishArchive(projectDir);
|
||||
const apiBaseUrl = getPublishApiBaseUrl();
|
||||
const stagedResult = await publishProjectArchiveStaged(apiBaseUrl, title, archive);
|
||||
const stagedResult = await publishProjectArchiveStaged(apiBaseUrl, title, archive, isPublic);
|
||||
if (stagedResult) return stagedResult;
|
||||
return publishProjectArchiveDirect(apiBaseUrl, title, archive);
|
||||
return publishProjectArchiveDirect(apiBaseUrl, title, archive, isPublic);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user