Skip to content
Merged
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
83 changes: 83 additions & 0 deletions src/actions/projects.create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ActionError, defineAction } from "astro:actions";
import { randomBytes } from "node:crypto";
import { z } from "astro/zod";
import { logger } from "@/server/logger";
import { getAvailableModels } from "@/server/opencode/models";
import { enqueueProjectCreate } from "@/server/queue/enqueue";

export const create = defineAction({
accept: "json",
input: z.object({
prompt: z.string().min(1, "Please describe your website"),
model: z.string().optional(),
attachments: z.string().optional(),
}),
handler: async (input, context) => {
const user = context.locals.user;
if (!user) {
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to create a project",
});
}

const availableModels = await getAvailableModels();
if (availableModels.length === 0) {
throw new ActionError({
code: "BAD_REQUEST",
message: "No models available. Please check your provider settings.",
});
}

let attachments:
| Array<{
filename: string;
mime: string;
dataUrl: string;
kind?: "image" | "text";
textContent?: string;
}>
| undefined;
if (input.attachments) {
try {
const parsed = JSON.parse(input.attachments);
if (!Array.isArray(parsed)) {
throw new ActionError({
code: "BAD_REQUEST",
message: "Invalid attachments format",
});
}
attachments = parsed;
} catch (err) {
if (err instanceof ActionError) throw err;
throw new ActionError({
code: "BAD_REQUEST",
message: "Invalid attachments JSON",
});
}
}
Comment thread
pablopunk marked this conversation as resolved.

const projectId = randomBytes(12).toString("hex");

try {
await enqueueProjectCreate({
projectId,
ownerUserId: user.id,
prompt: input.prompt,
model: input.model ?? null,
attachments,
});
} catch (err) {
logger.error({ err }, "Failed to enqueue project creation");
throw new ActionError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to start project creation",
});
}

return {
success: true,
projectId,
};
},
});
67 changes: 67 additions & 0 deletions src/actions/projects.crud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro/zod";
import {
getProjectById,
getProjectsByUserId,
} from "@/server/projects/projects.model";

export const list = defineAction({
handler: async (_input, context) => {
const user = context.locals.user;
if (!user) {
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to list projects",
});
}

const { getProjectRuntimeUrls } = await import(
"@/server/projects/projectUrls"
);
const projects = await getProjectsByUserId(user.id);
const projectsWithUrls = await Promise.all(
projects.map(async (project) => {
const urls = await getProjectRuntimeUrls(project);
return {
...project,
previewUrl: urls.preview.preferred,
previewUrls: urls.preview,
productionUrls: urls.production,
};
}),
);
return { projects: projectsWithUrls };
},
});

export const get = defineAction({
input: z.object({
projectId: z.string(),
}),
handler: async (input, context) => {
const user = context.locals.user;
if (!user) {
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to view a project",
});
}

const project = await getProjectById(input.projectId);
if (!project) {
throw new ActionError({
code: "NOT_FOUND",
message: "Project not found",
});
}

if (project.ownerUserId !== user.id) {
throw new ActionError({
code: "FORBIDDEN",
message: "You don't have access to this project",
});
}

return { project };
},
});
67 changes: 67 additions & 0 deletions src/actions/projects.delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro/zod";
import {
isProjectOwnedByUser,
updateProjectStatus,
} from "@/server/projects/projects.model";
import {
enqueueDeleteAllProjectsForUser,
enqueueProjectDelete,
} from "@/server/queue/enqueue";

export const deleteProject = defineAction({
input: z.object({
projectId: z.string(),
}),
handler: async (input, context) => {
const user = context.locals.user;
if (!user) {
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to delete a project",
});
}

const isOwner = await isProjectOwnedByUser(input.projectId, user.id);
if (!isOwner) {
throw new ActionError({
code: "FORBIDDEN",
message: "You don't have access to this project",
});
}

try {
await updateProjectStatus(input.projectId, "deleting");
} catch {}

try {
const { cancelActiveProductionJobs } = await import(
"@/server/productions/productions.model"
);
await cancelActiveProductionJobs(input.projectId);
} catch {}
Comment thread
pablopunk marked this conversation as resolved.

const job = await enqueueProjectDelete({
projectId: input.projectId,
requestedByUserId: user.id,
});

return { success: true, jobId: job.id };
},
});

export const deleteAll = defineAction({
handler: async (_input, context) => {
const user = context.locals.user;
if (!user) {
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to delete projects",
});
}

const job = await enqueueDeleteAllProjectsForUser({ userId: user.id });

return { success: true, jobId: job.id };
},
});
83 changes: 83 additions & 0 deletions src/actions/projects.lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro/zod";
import { isProjectOwnedByUser } from "@/server/projects/projects.model";
import { enqueueDockerStop } from "@/server/queue/enqueue";

export const stop = defineAction({
input: z.object({
projectId: z.string(),
}),
handler: async (input, context) => {
const user = context.locals.user;
if (!user) {
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to stop a project",
});
}

const isOwner = await isProjectOwnedByUser(input.projectId, user.id);
if (!isOwner) {
throw new ActionError({
code: "FORBIDDEN",
message: "You don't have access to this project",
});
}

const job = await enqueueDockerStop({
projectId: input.projectId,
reason: "user",
});

return { success: true, jobId: job.id };
},
});

export const restart = defineAction({
input: z.object({
projectId: z.string(),
}),
handler: async (input, context) => {
const user = context.locals.user;
if (!user) {
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to restart a project",
});
}

const isOwner = await isProjectOwnedByUser(input.projectId, user.id);
if (!isOwner) {
throw new ActionError({
code: "FORBIDDEN",
message: "You don't have access to this project",
});
}

const { getProjectById } = await import("@/server/projects/projects.model");
const project = await getProjectById(input.projectId);
if (!project) {
throw new ActionError({
code: "NOT_FOUND",
message: "Project not found",
});
}

const { enqueueDockerEnsureRunning } = await import(
"@/server/queue/enqueue"
);

const { updateProjectStatus } = await import(
"@/server/projects/projects.model"
);

const job = await enqueueDockerEnsureRunning({
projectId: input.projectId,
reason: "user",
});

await updateProjectStatus(input.projectId, "starting");

return { success: true, jobId: job.id };
},
});
48 changes: 48 additions & 0 deletions src/actions/projects.opencode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro/zod";
import { logger } from "@/server/logger";
import { restartGlobalOpencode } from "@/server/opencode/runtime";
import { isProjectOwnedByUser } from "@/server/projects/projects.model";

export const restartOpencode = defineAction({
accept: "json",
input: z.object({
projectId: z.string(),
}),
handler: async (input, context) => {
const user = context.locals.user;
if (!user) {
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to restart the agent",
});
}

const isOwner = await isProjectOwnedByUser(input.projectId, user.id);
if (!isOwner) {
throw new ActionError({
code: "NOT_FOUND",
message: "Project not found",
});
}

try {
logger.info(
{ projectId: input.projectId, userId: user.id },
"Restarting OpenCode agent",
);
await restartGlobalOpencode();
return { success: true };
Comment thread
pablopunk marked this conversation as resolved.
} catch (error) {
logger.error(
{ error, projectId: input.projectId },
"Failed to restart OpenCode agent",
);
throw new ActionError({
code: "INTERNAL_SERVER_ERROR",
message:
error instanceof Error ? error.message : "Failed to restart agent",
});
}
},
});
Loading
Loading