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'),
})
}