Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/planetscale

src/configure-loop.ts

148 lines · 4.9 KB · TypeScript
import { apiTokenSetupUrl, insightsUrl } from './setup.ts'
import { asLoopConfig, githubRefFromConfig } from './loop-helpers.ts'
import { readLoopConfig, writeLoopConfig } from './loop-storage.ts'
import type { LoopConfig } from './types.ts'
import {
	optionalBoolean,
	optionalString,
	optionalStringArray,
	requireRecord,
} from './types.ts'

export type ConfigureLoopInput = {
	organization?: string
	database?: string
	branch?: string
	githubOwner?: string
	githubRepo?: string
	repositoryUrl?: string
	cursorRef?: string
	account?: string
	githubIntegrationName?: string
	githubSecretName?: string
	labels?: Array<string>
	loopEnabled?: boolean
	dryRun?: boolean
	confirm?: boolean
}

export type ConfigureLoopResult = {
	ok: boolean
	dryRun: boolean
	wouldSave?: LoopConfig
	saved?: LoopConfig
	insightsUrl?: string
	github?: ReturnType<typeof githubRefFromConfig>
	next: Array<string>
}

function mergeConfig(current: LoopConfig, input: ConfigureLoopInput): LoopConfig {
	const next: LoopConfig = { ...current }
	const organization = optionalString(input.organization, 'organization')
	const database = optionalString(input.database, 'database')
	const branch = optionalString(input.branch, 'branch')
	const githubOwner = optionalString(input.githubOwner, 'githubOwner')
	const githubRepo = optionalString(input.githubRepo, 'githubRepo')
	const repositoryUrl = optionalString(input.repositoryUrl, 'repositoryUrl')
	const cursorRef = optionalString(input.cursorRef, 'cursorRef')
	const account = optionalString(input.account, 'account')
	const githubIntegrationName = optionalString(input.githubIntegrationName, 'githubIntegrationName')
	const githubSecretName = optionalString(input.githubSecretName, 'githubSecretName')
	const labels = optionalStringArray(input.labels, 'labels')
	const loopEnabled = optionalBoolean(input.loopEnabled, 'loopEnabled')
	if (organization) next.organization = organization
	if (database) next.database = database
	if (branch) next.branch = branch
	if (githubOwner) next.githubOwner = githubOwner
	if (githubRepo) next.githubRepo = githubRepo
	if (repositoryUrl) next.repositoryUrl = repositoryUrl
	if (cursorRef) next.cursorRef = cursorRef
	if (account) next.account = account
	if (githubIntegrationName) next.githubIntegrationName = githubIntegrationName
	if (githubSecretName) next.githubSecretName = githubSecretName
	if (labels) next.labels = labels
	if (loopEnabled !== undefined) next.loopEnabled = loopEnabled
	return next
}

function nextSteps(next: LoopConfig, dashboard?: string): Array<string> {
	const github = githubRefFromConfig(next)
	return [
		`Save a PlanetScale service token at ${apiTokenSetupUrl()}`,
		next.organization && next.database
			? `Confirm Insights at ${dashboard}`
			: 'Set organization + database (and usually branch).',
		github
			? `GitHub issues will go to ${github.fullName}`
			: 'Set githubOwner + githubRepo, or repositoryUrl, if you want run-loop to open issues.',
		github
			? `Cursor implementer will use ${github.repositoryUrl}`
			: 'Set repositoryUrl to a https GitHub URL if you want run-loop to launch an implementer.',
		next.loopEnabled
			? 'Webhook + poll-anomalies will create issues/PRs after confirm.'
			: 'loopEnabled is off. Webhook and poll-anomalies stay draft-only until you set loopEnabled: true with confirm.',
	]
}

/**
 * Save org/database/branch plus GitHub/Cursor targets in packageStorage.
 * Preview with dryRun (default). Persist only with confirm: true.
 * @example
 * import configureLoop from 'kody:@kody/planetscale/configure-loop'
 * await configureLoop({
 *   organization: 'acme',
 *   database: 'app',
 *   branch: 'main',
 *   githubOwner: 'acme',
 *   githubRepo: 'app',
 *   dryRun: true,
 * })
 */
export async function configureLoop(input: ConfigureLoopInput = {}): Promise<ConfigureLoopResult> {
	const current = await readLoopConfig()
	const next = mergeConfig(current, input)
	const dashboard =
		next.organization && next.database
			? insightsUrl(next.organization, next.database, next.branch ?? 'main')
			: undefined
	const github = githubRefFromConfig(next)
	const steps = nextSteps(next, dashboard)

	if (input.dryRun !== false && !input.confirm) {
		return {
			ok: true,
			dryRun: true,
			wouldSave: next,
			insightsUrl: dashboard,
			github,
			next: steps,
		}
	}

	const saved = await writeLoopConfig(next)
	return {
		ok: true,
		dryRun: false,
		saved,
		insightsUrl: dashboard,
		github,
		next: steps,
	}
}

/**
 * Save org/database/branch plus GitHub/Cursor targets in packageStorage.
 * @example
 * import configureLoop from 'kody:@kody/planetscale/configure-loop'
 * await configureLoop({ organization: 'acme', database: 'app', dryRun: true })
 */
export default async function configureLoopEntrypoint(
	params: Partial<ConfigureLoopInput> & Record<string, unknown> = {},
) {
	const input = requireRecord(params, 'configure-loop')
	return configureLoop({
		...asLoopConfig(input),
		dryRun: optionalBoolean(input.dryRun, 'dryRun'),
		confirm: optionalBoolean(input.confirm, 'confirm'),
	})
}