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> { try { const listed = await kody.secretList({ scope: 'user' }) const names = new Set() 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