import { kody } from 'kody:runtime'
import { listApps } from './apps.ts'
import {
API_TOKEN_SETUP_URL,
resolveApiTokenSecretName,
secretSetupUrl,
type FlyAuthOptions,
} from './fly-core.ts'
import { destroyMachine, startMachine, stopMachine } from './machines.ts'
import { listOrganizations } from './organizations.ts'
import machineCount from './machine-count.ts'
import type { FlySmokeTestInput } from './types.ts'
function secretEntries(result: unknown): Array<{ name?: string; scope?: string }> {
if (result && typeof result === 'object' && Array.isArray((result as { secrets?: unknown }).secrets)) {
return (result as { secrets: Array<{ name?: string; scope?: string }> }).secrets
}
return Array.isArray(result) ? result : []
}
async function listUserSecretNames(): Promise<Set<string>> {
try {
const listed = await kody.secret_list({ scope: 'user' })
const names = new Set<string>()
for (const entry of secretEntries(listed)) {
if (entry?.name && (entry.scope === 'user' || !entry.scope)) names.add(entry.name)
}
return names
} catch {
return new Set()
}
}
async function mutationSelfCheck() {
const target = { appName: 'my-app', machineId: 'machine_smoke' }
const start = await startMachine({ ...target, dryRun: true })
const stop = await stopMachine({ ...target, dryRun: true, timeout: 30 })
const destroy = await destroyMachine({ ...target, dryRun: true, force: true })
if (!('dryRun' in start) || start.dryRun !== true) {
throw new Error('startMachine dryRun self-check failed.')
}
if (!('dryRun' in stop) || stop.dryRun !== true) {
throw new Error('stopMachine dryRun self-check failed.')
}
if (!('dryRun' in destroy) || destroy.dryRun !== true) {
throw new Error('destroyMachine dryRun self-check failed.')
}
return { start: true, stop: true, destroy: true }
}
/**
* Local dry-run checks plus an optional live org/apps read.
*
* Never starts, stops, or destroys Machines.
*
* @example
* import smokeTest from 'kody:@kody/fly/smoke-test'
* const result = await smokeTest()
*/
export async function smokeTest(input: FlySmokeTestInput = {}) {
const dryRunMutations = await mutationSelfCheck()
const secretName = resolveApiTokenSecretName(input)
const names = await listUserSecretNames()
const hasToken = names.has(secretName)
if (!hasToken) {
return {
ok: true,
live: false,
selfCheck: { dryRunMutations },
secretName,
setup: {
auth: 'api-token',
hosts: ['api.machines.dev', 'api.fly.io'],
nextSteps: [
'Save a Fly API token as ' + secretName + '.',
secretSetupUrl(secretName),
'Approve hosts api.machines.dev and api.fly.io.',
],
},
}
}
const organizations = await listOrganizations({ ...input, first: 10 })
const orgSlug =
typeof input.organizationSlug === 'string' && input.organizationSlug.trim()
? input.organizationSlug.trim()
: organizations.organizations.length === 1
? organizations.organizations[0]?.slug
: undefined
const apps = orgSlug
? await listApps({ ...input, organizationSlug: orgSlug })
: { count: 0, apps: [], organization: { slug: null } }
const machines =
input.includeMachines === false || !orgSlug
? null
: await machineCount({ ...input, organizationSlug: orgSlug, maxPages: 1 })
return {
ok: true,
live: true,
selfCheck: { dryRunMutations },
secretName,
organizationCount: organizations.organizations.length,
organizations: organizations.organizations.map((org: any) => ({
id: org.id,
name: org.name,
slug: org.slug,
})),
organizationSlug: orgSlug ?? null,
appCount: apps.count,
appsSample: apps.apps.slice(0, 5).map((app: any) => ({ name: app.name, status: app.status })),
machineCount: machines?.count ?? null,
byState: machines?.byState ?? null,
setup: {
auth: 'api-token',
hosts: ['api.machines.dev', 'api.fly.io'],
apiTokenUrl: hasToken ? null : API_TOKEN_SETUP_URL,
},
}
}
export default smokeTest