import { ConvexError, v } from 'convex/values'; import type { Id } from './_generated/dataModel'; import type { QueryCtx } from './_generated/server'; import { internalQuery, mutation, query } from './_generated/server'; import { deriveHomeUsername, getRequiredUserId, optionalText } from './model'; const loadSettings = async (ctx: QueryCtx, ownerId: Id<'users'>) => await ctx.db .query('userEnvironment') .withIndex('by_owner', (q) => q.eq('ownerId', ownerId)) .unique(); /** Current user's environment settings + the resolved home username/first name. */ export const getMine = query({ args: {}, handler: async (ctx) => { const ownerId = await getRequiredUserId(ctx); const [user, settings] = await Promise.all([ ctx.db.get(ownerId), loadSettings(ctx, ownerId), ]); const firstName = (user?.name ?? '').trim().split(/\s+/)[0] || 'you'; const username = settings?.homeUsername ?? deriveHomeUsername(user?.name); return { enabled: settings?.enabled ?? true, username, firstName, dotfilesRepoUrl: settings?.dotfilesRepoUrl, dotfilesRepoRef: settings?.dotfilesRepoRef, setupCommand: settings?.setupCommand, }; }, }); export const updateMine = mutation({ args: { enabled: v.optional(v.boolean()), dotfilesRepoUrl: v.optional(v.string()), dotfilesRepoRef: v.optional(v.string()), setupCommand: v.optional(v.string()), }, handler: async (ctx, args) => { const ownerId = await getRequiredUserId(ctx); const repoUrl = optionalText(args.dotfilesRepoUrl); if (repoUrl && !/^https?:\/\//.test(repoUrl)) { throw new ConvexError('Dotfiles repo must be a public http(s) URL.'); } const existing = await loadSettings(ctx, ownerId); const patch = { enabled: args.enabled ?? existing?.enabled ?? true, dotfilesRepoUrl: repoUrl, dotfilesRepoRef: optionalText(args.dotfilesRepoRef), setupCommand: optionalText(args.setupCommand), updatedAt: Date.now(), }; if (existing) { await ctx.db.patch(existing._id, patch); return { success: true }; } await ctx.db.insert('userEnvironment', { ownerId, ...patch }); return { success: true }; }, }); // Worker-facing: everything needed to materialize a job's owner's environment. // Content stays encrypted here; the Node action decrypts it. Resolves the owner // from the job. export const getRawEnvironmentForJobInternal = internalQuery({ args: { jobId: v.id('agentJobs') }, handler: async (ctx, { jobId }) => { const job = await ctx.db.get(jobId); if (!job) return null; const ownerId = job.ownerId; const [user, settings, files] = await Promise.all([ ctx.db.get(ownerId), loadSettings(ctx, ownerId), ctx.db .query('userDotfiles') .withIndex('by_owner', (q) => q.eq('ownerId', ownerId)) .collect(), ]); return { username: settings?.homeUsername ?? deriveHomeUsername(user?.name), enabled: settings?.enabled ?? true, dotfilesRepoUrl: settings?.dotfilesRepoUrl, dotfilesRepoRef: settings?.dotfilesRepoRef, setupCommand: settings?.setupCommand, files: files.map((f) => ({ path: f.path, encryptedContent: f.encryptedContent, isExecutable: f.isExecutable ?? false, })), }; }, });