type-check komodo actions against real client types
107 unmodified linesExpand all1081091101111121131141151161171121131141151161171181191201211221231241251261271281291301311321331341357 unmodified lines1431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721733 unmodified lines107 unmodified linesExpand allcredentials. Comparing the render is what catches a service being wired tothe wrong one; printing it is what leaks one. Differences outside `image:`lines are reported by key only, and abort.
**`RunAction` is asynchronous and its `success` field lies.** It returns inwell under a second with `success: true, status: InProgress` — that is theinitial value of a record the release has not started writing, not aresult. Gating CI on it makes every tag go green regardless of outcome. Theworkflow captures the update id and polls `read/GetUpdate` until`status: Complete`, then reads `success` from there.**Komodo's execute calls do not wait, and their `success` field lies.** Anexecute returns the Update in its initial state — `success: true,status: InProgress` — before the work has begun. Gating on it makes a wholerelease "succeed" in ten seconds having built and deployed nothing. Insidean Action use the client's own `execute_and_poll`; from CI, capture theupdate id and poll `read/GetUpdate` until `status: Complete`, which is whatthe workflow does.
**Compose is not guaranteed to pull.** Some services carry`pull_policy: never` from before images came from a registry, so`compose up` skips the pull and then dies with a bare "No such image" thatnames no cause. The Action pulls explicitly on each host first, so aregistry or credential problem surfaces as a registry error on the hostthat has it, before anything is torn down.
**Komodo's reported service image is rendered, not observed.** When itrenders without the stack's env file the tag interpolates to nothing andcomes back as `:0`, so a healthy service reads as a failed deploy.Verification asks the container (`InspectStackContainer`) instead.
**Giving up on waiting is not cancelling.** If the poll loop hits itsdeadline the release continues server-side. Treat it as *unknown* and readthe Update before re-running, because a blind retry redeploys.
7 unmodified linesthe Action is idempotent, so re-running a previous version is safe. Everycompose file `wisp-prepare-tag` touches is backed up alongside it as`<name>.bak-wisptag-<UTC stamp>`.
## Type-checking the Actions
`./deploy/typecheck.sh` checks both Actions against Komodo's real clienttypes. An Action is not a module — Komodo pastes its contents inside`async function main()` and supplies the globals — so the script wraps eachfile the same way before checking it, and `komodo-globals.d.ts` declareswhat Komodo injects. Subtract 2 from a reported line to map it back.
The types under `komodo/vendor/` are fetched from a running Komodo andcommitted so the check works offline:
```shfor f in types lib responses terminal; do curl -sL "$KOMODO_ADDRESS/client/$f.d.ts" -o deploy/komodo/vendor/$f.d.tsdone```
Re-fetch them after a Komodo upgrade. This is worth keeping honest: typingthese against the real client is what surfaced `execute_and_poll` (whichreplaced a hand-rolled poller), an Update id read as `.id` when it is`_id.$oid` (so every failure message printed `undefined`), and severaloptional fields being read as if they were guaranteed.
## Open
- Sync the Actions via ResourceSync TOML so changes to these files land as PRs instead of a UI paste.3 unmodified lines1234567891011121314{ "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM"], "module": "Preserve", "moduleResolution": "bundler", "allowImportingTsExtensions": true, "strict": true, "noEmit": true, "skipLibCheck": true, "types": [] }, "include": ["komodo/**/*.ts"]}12345678910111213141516171819202122232425262728293031323334353637383940414243444546#!/usr/bin/env bash## Type-check the Komodo Actions.## An Action file is not a module — Komodo pastes its contents inside an# `async function main()` and supplies `ARGS`, `komodo`, `Types`, `YAML` and# `TOML` around it. Checking the files as-is therefore reports noise that is# not real (top-level await "errors", and every Action colliding with every# other at global scope) while hiding what matters.## So each Action is wrapped exactly the way Komodo wraps it and the wrapper# is what gets checked. Line numbers are shifted by the wrapper preamble;# subtract 2 to map a reported line back to the source file.## Types under komodo/vendor are fetched from a running Komodo:# for f in types lib responses terminal; do# curl -sL "https://<komodo>/client/$f.d.ts" -o deploy/komodo/vendor/$f.d.ts# done
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"work="$(mktemp -d)"trap 'rm -rf "$work"' EXIT
mkdir -p "$work/komodo"cp -R "$here/komodo/vendor" "$work/komodo/vendor"cp "$here/komodo/komodo-globals.d.ts" "$work/komodo/"
for action in "$here"/komodo/*.ts; do name="$(basename "$action")" # declaration files describe the wrapper; they must not be wrapped by it case "$name" in *.d.ts) continue ;; esac { echo "export {}" echo "async function main() {" cat "$action" echo "}" echo "void main" } > "$work/komodo/$name"done
cp "$here/tsconfig.json" "$work/tsconfig.json"
bunx tsc -p "$work/tsconfig.json"echo "actions type-check clean"12345678910111213141516171819202122232425262728293031323334353637383940414243// The globals Komodo injects into an Action script.//// Komodo wraps an Action's file contents in a preamble that imports its// client, declares `ARGS` from the request, and puts the body inside// `async function main()`. None of that is visible in the file itself, so// these declarations stand in for it and let the Actions be type-checked// here rather than only in Komodo's editor.//// Keep in step with Komodo's wrapper (bin/core/src/api/execute/action.rs).// The client types under vendor/ are fetched from a running Komodo:// curl https://<komodo>/client/{types,lib,responses,terminal}.d.ts
import type { KomodoClient } from './vendor/lib.js'import type * as KomodoTypes from './vendor/types.js'
declare global { /** Arguments merged from the Action's defaults and the RunAction request. */ const ARGS: Record<string, unknown>
/** Pre-authenticated client. Actions get one; no keys are configured. */ const komodo: ReturnType<typeof KomodoClient>
// `export import` so `Types` works as both a value and a type // namespace — `Types.Version` as an annotation, `Types.SomeEnum` as a // value, exactly as it behaves inside Komodo. export import Types = KomodoTypes
const YAML: { stringify(value: unknown): string parse(text: string): unknown parseAll(text: string): unknown[] parseDockerCompose(text: string): unknown }
const TOML: { stringify(value: unknown): string parse(text: string): unknown parseResourceToml(text: string): unknown parseCargoToml(text: string): unknown }}
export {}29 unmodified lines3031323334353637383940414243444546474845 unmodified lines94959697879899100101102103104941051061071081091101111123 unmodified lines1161171181191061071201211221231241252 unmodified lines1281291301311181191321331341351361371381391401 unmodified line1421431441451291301311461471481491504 unmodified lines15515615715814415916016116216361 unmodified lines29 unmodified lines const colon = ref.lastIndexOf(':') return colon > slash ? ref.slice(0, colon) : ref}
// Komodo types these as optional, and they genuinely can be absent — a// resource can exist without a readable config. Failing here names the// resource; letting it through produces an "undefined is not an object"// halfway into a deploy.const required = <T>(value: T | null | undefined, what: string): T => { if (value === null || value === undefined) { throw new Error(`Komodo returned no ${what}`) } return value}
const imageTag = (ref: string): string => { const slash = ref.lastIndexOf('/') const colon = ref.lastIndexOf(':') return colon > slash ? ref.slice(colon + 1) : ''45 unmodified lines}
// ------------------------------------------------ discover what we build
const builds = (await komodo.read('ListBuilds', {})).filter((b: any) => b.info?.repo === repo)const builds = (await komodo.read('ListBuilds', {})).filter((b) => b.info?.repo === repo)if (!builds.length) { throw new Error(`no Komodo build clones '${repo}'`)}
const images: string[] = []for (const build of builds) { const config = (await komodo.read('GetBuild', { build: build.name })).config const config = required( (await komodo.read('GetBuild', { build: build.name })).config, `config for build ${build.name}`, ) const registry = (config.image_registry ?? [])[0] if (!registry?.domain) continue images.push( [registry.domain, registry.organization || registry.account, config.image_name || build.name]3 unmodified lines}
const ours = (image: string) => images.includes(imageRepo(image))
const stacks = (await komodo.read('ListStacks', {})).filter((s: any) => (s.info?.services ?? []).some((svc: any) => svc.image && ours(svc.image)),const stacks = (await komodo.read('ListStacks', {})).filter((s) => (s.info?.services ?? []).some((svc) => svc.image && ours(svc.image)),)if (!stacks.length) { throw new Error(`no stack runs any of: ${images.join(', ')}`)}2 unmodified lines
// -------------------------------------------------------- per stack
for (const stack of stacks) { const server = stack.info?.server_name const config = (await komodo.read('GetStack', { stack: stack.name })).config const server = required(stack.info?.server_name, `server for stack ${stack.name}`) const config = required( (await komodo.read('GetStack', { stack: stack.name })).config, `config for stack ${stack.name}`, ) const dir = config.run_directory const files: string[] = config.file_paths ?? []
console.log(`\n── ${stack.name} (${server})`)1 unmodified line // The seed keeps this stack exactly where it already is. It can only be // inferred when every one of our services here is on the same tag; if // they are drifted, picking one silently rolls the others back. const tags = [ ...new Set( (stack.info?.services ?? []).filter((s: any) => s.image && ours(s.image)).map((s: any) => imageTag(s.image)), ), ...new Set((stack.info?.services ?? []).filter((s) => s.image && ours(s.image)).map((s) => imageTag(s.image))), ] const seed = seedArg || (tags.length === 1 ? (tags[0] as string) : '') if (!seed) { throw new Error(4 unmodified lines
// Every env file Komodo passes, in its order, so the render matches what // actually deploys. Komodo's own env file may not exist until the first // deploy writes it, so only pass the ones that are actually there. const envFiles = [config.env_file_path, ...(config.additional_env_files ?? []).map((a: any) => a.path)] const envFiles = [config.env_file_path, ...(config.additional_env_files ?? []).map((a) => a.path)] .filter(Boolean) .map((p: string) => (p.startsWith('/') ? p : `${dir}/${p}`))
// A grep-style alternation of our image repos, for sed and for grep.61 unmodified lines27 unmodified lines282930313233343536372 unmodified lines40414243444546474849502 unmodified lines535455565758596061626364656667686970712 unmodified lines7475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126641271281291301312 unmodified lines1341351361377513813914014114214314414512 unmodified lines158159160161961621631641651668 unmodified lines1751761771781131791801811161821831181841851861871882 unmodified lines1911921931941291301951961971981992004 unmodified lines2052062072082092102112121442132142152162172182192202212222232242252262271502282292302312322332342352362372382392402412422432442452462472482492502512522532542551522562572582592608 unmodified lines2692702712721692732742752762772781751761771781791802792802812822832842851822862872882891862902912922932942952962972982993003013023033043051961973063073083093103113123133143153163173183193203213223233243258 unmodified lines27 unmodified linesconst version = String(ARGS.version ?? '').trim()const commit = String(ARGS.commit ?? '').trim()const repo = String(ARGS.repo ?? '').trim()
const [major, minor, patch] = version.split('.').map(Number)
if (!/^\d+\.\d+\.\d+$/.test(version)) { throw new Error(`bad version arg: '${version}' (want major.minor.patch)`)}if (!/^[0-9a-f]{40}$/.test(commit)) {2 unmodified linesif (!repo) { throw new Error('missing repo arg (the source repo the builds clone)')}
// Komodo stores a build version as a struct, not a string.const semver: Types.Version = { major, minor, patch }
// ---------------------------------------------------------------- helpers
// Strip the tag from an image reference, leaving the repository path.// A colon only introduces a tag when it sits in the last path segment.2 unmodified lines const colon = ref.lastIndexOf(':') return colon > slash ? ref.slice(0, colon) : ref}
// Komodo types these as optional, and they genuinely can be absent — a// resource can exist without a readable config. Failing here names the// resource; letting it through produces an "undefined is not an object"// halfway into a deploy.const required = <T>(value: T | null | undefined, what: string): T => { if (value === null || value === undefined) { throw new Error(`Komodo returned no ${what}`) } return value}
// Replace or append a single KEY=value line, leaving every other line alone.const withVar = (env: string, key: string, value: string): string => { const kept = (env ?? '') .split('\n')2 unmodified lines .trimEnd() return `${kept ? `${kept}\n` : ''}${key}=${value}\n`}
// `komodo.execute(...)` DOES NOT WAIT. It returns the Update record in its// initial state — `success: true, status: "InProgress"` — before the work// has begun, so reading `.success` off it is meaningless: a whole release// will "succeed" in ten seconds having built and deployed nothing.//// `execute_and_poll` is the client's own version that waits for the Update// to reach Complete. Use it for anything whose result matters. It can also// return a batch array, which none of the calls here do — `one()` narrows// that away rather than leaving the union to be indexed blindly.const one = (result: Awaited<ReturnType<typeof komodo.execute_and_poll>>): Types.Update => { if (Array.isArray(result)) { throw new Error('expected a single update, got a batch result') } return result}
// An Update's id is `_id.$oid`, not `.id`. Getting this wrong prints// "undefined" into exactly the error that is supposed to point at the run.const updateRef = (update: Types.Update): string => update._id?.$oid ?? 'unknown'
// The server terminal is a live bash session: it appends its exit-code// marker to the LAST LINE sent, echoes the prompt between lines, and hangs// forever if a line leaves bash awaiting input. So a script goes over as// exactly one line, base64-encoded into a fresh non-interactive bash, and// the marker then reflects that bash's status.const sh = async (server: string, script: string): Promise<string> => { const encoded = btoa(String.fromCharCode(...new TextEncoder().encode(script))) const lines: string[] = [] let code: string | undefined await komodo.execute_server_terminal( { server, command: `printf %s '${encoded}' | base64 -d | bash`, init: { command: 'bash' } }, { onLine: (line) => { lines.push(line) }, onFinish: (c) => { code = c }, }, ) const out = lines.join('\n').trim() if (code !== '0') { throw new Error(`[${server}] exit ${code ?? '?'}\n${out}`) } return out}
// ------------------------------------------- 1. discover what we build
const builds = (await komodo.read('ListBuilds', {})).filter((b: any) => b.info?.repo === repo)const builds = (await komodo.read('ListBuilds', {})).filter((b) => b.info?.repo === repo)
if (!builds.length) { throw new Error(`no Komodo build clones '${repo}' — nothing to release`)}2 unmodified lines// what they actually run rather than by name.const images: string[] = []
for (const build of builds) { const config = (await komodo.read('GetBuild', { build: build.name })).config const config = required( (await komodo.read('GetBuild', { build: build.name })).config, `config for build ${build.name}`, ) const registry = (config.image_registry ?? [])[0] if (!registry?.domain) { throw new Error(`build ${build.name} has no image registry configured`) }12 unmodified lines// right now, which is not necessarily what was tagged.for (const build of builds) { await komodo.write('UpdateBuild', { id: build.name, config: { version: version as any, auto_increment_version: false, commit }, config: { version: semver, auto_increment_version: false, commit }, })}
// ------------------------------------------------------ 3. build images8 unmodified lines// against the now-warm cache, not to change any config.for (const build of builds) { let ok = false for (let attempt = 1; attempt <= 2 && !ok; attempt++) { const update = await komodo.execute('RunBuild', { build: build.name }) const update = one(await komodo.execute_and_poll('RunBuild', { build: build.name })) ok = update.success if (!ok) { console.log(`build ${build.name} failed (attempt ${attempt}/2), update ${update.id}`) console.log(`build ${build.name} failed (attempt ${attempt}/2), update ${updateRef(update)}`) if (attempt === 2) { throw new Error(`build ${build.name} failed twice — see Komodo update ${update.id}`) throw new Error(`build ${build.name} failed twice — see Komodo update ${updateRef(update)}`) } } } console.log(`built ${build.name}:${version}`)2 unmodified lines// -------------------------------------- 4. find the stacks that run them
const ours = (image: string) => images.includes(imageRepo(image))
const stacks = (await komodo.read('ListStacks', {})).filter((s: any) => (s.info?.services ?? []).some((svc: any) => svc.image && ours(svc.image)),const stacks = (await komodo.read('ListStacks', {})).filter((s) => (s.info?.services ?? []).some((svc) => svc.image && ours(svc.image)),)
if (!stacks.length) { throw new Error(`built ${version} but no stack runs any of: ${images.join(', ')}`)4 unmodified lines// The tag moves through the stack's own environment, which Komodo owns and// writes out at deploy time. Compose files are hand-maintained and carry// per-host detail this release knows nothing about, so nothing here renders,// templates or rewrites one.// What each service was on before this deploy, so verification can tell// "already correct, nothing to do" apart from "never actually deployed".const before = new Map<string, string>()for (const stack of stacks) { const config = (await komodo.read('GetStack', { stack: stack.name })).config for (const svc of stack.info?.services ?? []) { if (svc.image && ours(svc.image)) before.set(`${stack.name}/${svc.service}`, svc.image) }}
for (const stack of stacks) { const config = required( (await komodo.read('GetStack', { stack: stack.name })).config, `config for stack ${stack.name}`, ) await komodo.write('UpdateStack', { id: stack.name, config: { environment: withVar(config.environment ?? '', TAG_VAR, version) }, })
const update = await komodo.execute('DeployStack', { stack: stack.name }) // Pull explicitly, on the host, before deploying. // // Compose cannot be relied on to fetch these: some services carry // `pull_policy: never` from before images came from a registry, so // `compose up` skips the pull and then dies with a bare // "No such image" that names no cause. Pulling here means a registry or // credential problem surfaces as a registry error, on the host that has // the problem, before anything is torn down. const wanted = [ ...new Set( (stack.info?.services ?? []) .map((s) => s.image) .filter((i: string) => i && ours(i)) .map((i: string) => `${imageRepo(i)}:${version}`), ), ]
const pull = [ 'set -euo pipefail', ...wanted.map((image) => `echo "pulling ${image}"; docker pull -q '${image}' >/dev/null`), ].join('\n')
const server = required(stack.info?.server_name, `server for stack ${stack.name}`) await sh(server, pull) console.log(`pulled ${wanted.length} images on ${server}`)
const update = one(await komodo.execute_and_poll('DeployStack', { stack: stack.name })) if (!update.success) { throw new Error(`DeployStack ${stack.name} failed — see Komodo update ${update.id}`) throw new Error(`DeployStack ${stack.name} failed — see Komodo update ${updateRef(update)}`) } console.log(`deployed ${stack.name}`)}
8 unmodified linesconst problems: string[] = []const deployed = await komodo.read('ListStacks', {})
for (const stack of stacks) { const services = (deployed.find((s: any) => s.name === stack.name)?.info?.services ?? []) as any[] const services = deployed.find((s) => s.name === stack.name)?.info?.services ?? []
for (const svc of services.filter((s) => s.image && ours(s.image))) { const want = `${imageRepo(svc.image)}:${version}` const where = `${stack.name}/${svc.service}`
if (svc.image !== want) { problems.push(`${where}: on ${svc.image}, expected ${want}`) continue }
let state: any // The image is read off the container, not off the stack listing. // Komodo's listing reports a *rendered* image, and when it renders // without the stack's env file the tag interpolates to nothing and // surfaces as ":0" — a healthy service then looks like a failed // deploy. The container knows what it is actually running. let container: Types.InspectStackContainerResponse try { const inspected: any = await komodo.read('InspectStackContainer', { container = await komodo.read('InspectStackContainer', { stack: stack.name, service: svc.service, }) state = (inspected.container ?? inspected).State ?? {} } catch (error) { problems.push(`${where}: not inspectable — ${error}`) continue }
const state = container.State ?? {} const running = container.Config?.Image
if (running !== want) { problems.push(`${where}: on ${running}, expected ${want}`) continue } if (!state.Running) { problems.push(`${where}: image is correct but the container is not running`) continue } const age = Math.round((Date.now() - Date.parse(state.StartedAt)) / 1000) if (age > MAX_CONTAINER_AGE_SECONDS) { // Only demand a recent restart where the tag actually moved. Re-running // a release that is already live is legitimate and recreates nothing, // so an old uptime there is correct rather than suspicious. const startedAt = state.StartedAt if (!startedAt) { problems.push(`${where}: container reports no start time`) continue } const age = Math.round((Date.now() - Date.parse(startedAt)) / 1000) const was = before.get(where) // A pre-deploy image the listing could not render (":0" or a bare // ":") says nothing about whether this moved, so the age check is // skipped rather than failed on a guess. const knownBefore = was && !/:(0)?$/.test(was) const moved = knownBefore && was !== want if (moved && age > MAX_CONTAINER_AGE_SECONDS) { problems.push(`${where}: tagged ${version} but up for ${age}s — never recreated`) continue } console.log(`ok ${where} ${version} (up ${age}s)`)8 unmodified lines123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428import { ExecuteResponses, ReadResponses, WriteResponses } from "./responses.js";import { TerminalCallbacks } from "./terminal.js";import { ConnectTerminalQuery, ExecuteRequest, ExecuteTerminalBody, ReadRequest, Update, UpdateListItem, User, WriteRequest } from "./types.js";export * as MoghAuth from "npm:mogh_auth_client";export * as Types from "./types.js";export type { ExecuteResponses, ReadResponses, WriteResponses, TerminalCallbacks, };export type InitOptions = { type: "jwt"; params: { jwt: string; };} | { type: "api-key"; params: { key: string; secret: string; };};export declare class CancelToken { cancelled: boolean; constructor(); cancel(): void;}export type ClientState = { jwt: string | undefined; key: string | undefined; secret: string | undefined;};/** Initialize a new client for Komodo */export declare function KomodoClient(url: string, options: InitOptions): { /** * Call the `/auth` api. * * ``` * const { jwt } = await komodo.auth.login("LoginLocalUser", { * username: "test-user", * password: "test-pass" * }); * ``` * * https://docs.rs/mogh_auth_client/latest/mogh_auth_client/api/index.html */ auth: { login: <T extends import("mogh_auth_client/dist/types.js").LoginRequest["type"], Req extends Extract<import("mogh_auth_client/dist/types.js").LoginRequest, { type: T; }>>(type: T, params: Req["params"]) => Promise<import("mogh_auth_client").LoginResponses[Req["type"]]>; manage: <T extends import("mogh_auth_client/dist/types.js").ManageRequest["type"], Req extends Extract<import("mogh_auth_client/dist/types.js").ManageRequest, { type: T; }>>(type: T, params: Req["params"]) => Promise<import("mogh_auth_client").ManageResponses[Req["type"]]>; externalLogin: (provider: import("mogh_auth_client/dist/types.js").ExternalLoginProvider) => void; }; /** * Get the current (calling) user. * * ``` * const user = await komodo.getUser(); * ``` * * https://docs.rs/komodo_client/latest/komodo_client/api/user/index.html */ getUser: () => Promise<User>; /** * Call the `/read` api. * * ``` * const stack = await komodo.read("GetStack", { * stack: "my-stack" * }); * ``` * * https://docs.rs/komodo_client/latest/komodo_client/api/read/index.html */ read: <T extends ReadRequest["type"], Req extends Extract<ReadRequest, { type: T; }>>(type: T, params: Req["params"]) => Promise<ReadResponses[Req["type"]]>; /** * Call the `/write` api. * * ``` * const build = await komodo.write("UpdateBuild", { * id: "my-build", * config: { * version: "1.0.4" * } * }); * ``` * * https://docs.rs/komodo_client/latest/komodo_client/api/write/index.html */ write: <T extends WriteRequest["type"], Req extends Extract<WriteRequest, { type: T; }>>(type: T, params: Req["params"]) => Promise<WriteResponses[Req["type"]]>; /** * Call the `/execute` api. * * ``` * const update = await komodo.execute("DeployStack", { * stack: "my-stack" * }); * ``` * * NOTE. These calls return immediately when the update is created, NOT when the execution task finishes. * To have the call only return when the task finishes, use [execute_and_poll_until_complete]. * * https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html */ execute: <T extends ExecuteRequest["type"], Req extends Extract<ExecuteRequest, { type: T; }>>(type: T, params: Req["params"]) => Promise<ExecuteResponses[Req["type"]]>; /** * Call the `/execute` api, and poll the update until the task has completed. * * ``` * const update = await komodo.execute_and_poll("DeployStack", { * stack: "my-stack" * }); * ``` * * https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html */ execute_and_poll: <T extends ExecuteRequest["type"], Req extends Extract<ExecuteRequest, { type: T; }>>(type: T, params: Req["params"]) => Promise<(Update | { status: "Err"; data: import("./types.js").BatchExecutionResponseItemErr; })[] | Update>; /** * Poll an Update (returned by the `execute` calls) until the `status` is `Complete`. * https://docs.rs/komodo_client/latest/komodo_client/entities/update/struct.Update.html#structfield.status. */ poll_update_until_complete: (update_id: string) => Promise<Update>; /** Returns the version of Komodo Core the client is calling to. */ core_version: () => Promise<string>; /** * Connects to update websocket, performs login and attaches handlers, * and returns the WebSocket handle. */ get_update_websocket: ({ on_update, on_login, on_open, on_close, }: { on_update: (update: UpdateListItem) => void; on_login?: () => void; on_open?: () => void; on_close?: () => void; }) => WebSocket; /** * Subscribes to the update websocket with automatic reconnect loop. * * Note. Awaiting this method will never finish. */ subscribe_to_update_websocket: ({ on_update, on_open, on_login, on_close, retry, retry_timeout_ms, cancel, on_cancel, }: { on_update: (update: UpdateListItem) => void; on_login?: () => void; on_open?: () => void; on_close?: () => void; retry?: boolean; retry_timeout_ms?: number; cancel?: CancelToken; on_cancel?: () => void; }) => Promise<void>; /** * Subscribes to terminal io over websocket message, * for use with xtermjs. */ connect_terminal: ({ query: { target, terminal, init }, on_message, on_login, on_open, on_close, }: { query: ConnectTerminalQuery; } & TerminalCallbacks) => WebSocket; /** * Executes a command on a given target / terminal, * and gives callbacks to handle the output as it comes in. * * ```ts * await komodo.execute_terminal( * { * target: { * type: "Server", * params: { * server: "my-server" * } * }, * terminal: "name", * command: 'for i in {1..3}; do echo "$i"; sleep 1; done', * init: { * command: "bash", * recreate: Types.TerminalRecreateMode.Always * } * }, * { * onLine: (line) => console.log(line), * onFinish: (code) => console.log("Finished:", code), * } * ); * ``` */ execute_terminal: (request: ExecuteTerminalBody, callbacks?: import("./terminal.js").ExecuteCallbacks) => Promise<void>; /** * Executes a command on a given target / terminal, * and returns a stream to process the output as it comes in. * * Note. The final line of the stream will usually be * `__KOMODO_EXIT_CODE__:0`. The number * is the exit code of the command. * * If this line is NOT present, it means the stream * was terminated early, ie like running `exit`. * * ```ts * const stream = await komodo.execute_terminal_stream({ * target: { * type: "Server", * params: { * server: "my-server" * } * }, * terminal: "name", * command: 'for i in {1..3}; do echo "$i"; sleep 1; done', * init: { * command: "bash", * recreate: Types.TerminalRecreateMode.Always * } * }); * * for await (const line of stream) { * console.log(line); * } * ``` */ execute_terminal_stream: (request: ExecuteTerminalBody) => Promise<AsyncIterable<string>>; /** * Executes a command on a given Server / terminal, * and gives callbacks to handle the output as it comes in. * * ```ts * await komodo.execute_server_terminal( * { * server: "my-server", * terminal: "name", * command: 'for i in {1..3}; do echo "$i"; sleep 1; done', * init: { * command: "bash", * recreate: Types.TerminalRecreateMode.Always * } * }, * { * onLine: (line) => console.log(line), * onFinish: (code) => console.log("Finished:", code), * } * ); * ``` */ execute_server_terminal: ({ server, terminal, command, init, }: { server: string; terminal?: string; command: string; init?: import("./types.js").InitTerminal; }, callbacks?: import("./terminal.js").ExecuteCallbacks) => Promise<void>; /** * Executes a command on a given Server / Container / terminal, * and gives callbacks to handle the output as it comes in. * * ```ts * await komodo.execute_container_terminal( * { * server: "my-server", * container: "my-container", * terminal: "name", * command: 'for i in {1..3}; do echo "$i"; sleep 1; done', * init: { * command: "bash", * recreate: Types.TerminalRecreateMode.Always * } * }, * { * onLine: (line) => console.log(line), * onFinish: (code) => console.log("Finished:", code), * } * ); * ``` */ execute_container_terminal: ({ server, container, terminal, command, init, }: { server: string; container: string; terminal?: string; command: string; init?: import("./types.js").InitTerminal; }, callbacks?: import("./terminal.js").ExecuteCallbacks) => Promise<void>; /** * Executes a command on a given Stack / service / terminal, * and gives callbacks to handle the output as it comes in. * * ```ts * await komodo.execute_stack_service_terminal( * { * stack: "my-stack", * service: "my-service", * terminal: "name", * command: 'for i in {1..3}; do echo "$i"; sleep 1; done', * init: { * command: "bash", * recreate: Types.TerminalRecreateMode.Always * } * }, * { * onLine: (line) => console.log(line), * onFinish: (code) => console.log("Finished:", code), * } * ); * ``` */ execute_stack_service_terminal: ({ stack, service, terminal, command, init, }: { stack: string; service: string; terminal?: string; command: string; init?: import("./types.js").InitTerminal; }, callbacks?: import("./terminal.js").ExecuteCallbacks) => Promise<void>; /** * Executes a command on a given Deployment / terminal, * and gives callbacks to handle the output as it comes in. * * ```ts * await komodo.execute_deployment_terminal( * { * deployment: "my-deployemnt", * terminal: "name", * command: 'for i in {1..3}; do echo "$i"; sleep 1; done', * init: { * command: "bash", * recreate: Types.TerminalRecreateMode.Always * } * }, * { * onLine: (line) => console.log(line), * onFinish: (code) => console.log("Finished:", code), * } * ); * ``` */ execute_deployment_terminal: ({ deployment, terminal, command, init, }: { deployment: string; terminal?: string; command: string; init?: import("./types.js").InitTerminal; }, callbacks?: import("./terminal.js").ExecuteCallbacks) => Promise<void>; /** * Executes a command on a given Server / Container / terminal, * and gives callbacks to handle the output as it comes in. * * ```ts * await komodo.execute_container_exec( * { * server: "my-server", * container: "my-container", * command: 'for i in {1..3}; do echo "$i"; sleep 1; done', * shell: "bash", * terminal: "name", * recreate: Types.TerminalRecreateMode.Always, * }, * { * onLine: (line) => console.log(line), * onFinish: (code) => console.log("Finished:", code), * } * ); * ``` */ execute_container_exec: ({ server, container, shell, command, terminal, recreate, }: { server: string; container: string; shell: string; command: string; terminal?: string; recreate?: import("./types.js").TerminalRecreateMode; }, callbacks?: import("./terminal.js").ExecuteCallbacks) => Promise<void>; /** * Executes a command on a given Deployment / terminal, * and gives callbacks to handle the output as it comes in. * * ```ts * await komodo.execute_deployment_exec( * { * deployment: "my-deployemnt", * command: 'for i in {1..3}; do echo "$i"; sleep 1; done', * shell: "bash", * terminal: "name", * recreate: Types.TerminalRecreateMode.Always, * }, * { * onLine: (line) => console.log(line), * onFinish: (code) => console.log("Finished:", code), * } * ); * ``` */ execute_deployment_exec: ({ deployment, shell, command, terminal, recreate, }: { deployment: string; shell: string; command: string; terminal?: string; recreate?: import("./types.js").TerminalRecreateMode; }, callbacks?: import("./terminal.js").ExecuteCallbacks) => Promise<void>; /** * Executes a command on a given Stack / service / terminal, * and gives callbacks to handle the output as it comes in. * * ```ts * await komodo.execute_stack_exec( * { * stack: "my-stack", * service: "my-service", * command: 'for i in {1..3}; do echo "$i"; sleep 1; done', * shell: "bash", * terminal: "name", * recreate: Types.TerminalRecreateMode.Always * }, * { * onLine: (line) => console.log(line), * onFinish: (code) => console.log("Finished:", code), * } * ); * ``` */ execute_stack_exec: ({ stack, service, shell, command, terminal, recreate, }: { stack: string; service: string; shell: string; command: string; terminal?: string; recreate?: import("./types.js").TerminalRecreateMode; }, callbacks?: import("./terminal.js").ExecuteCallbacks) => Promise<void>;};12345678910111213141516171819202122232425// Stubs for the auth client the vendored Komodo types reference.//// Komodo's client pulls `mogh_auth_client` from npm for its login surface.// Nothing in deploy/komodo touches auth — the Action runtime hands us an// already-authenticated client — so the package is stubbed rather than// installed. If a script here ever needs `komodo.auth`, install the real// package instead of widening these.
declare module 'npm:mogh_auth_client' { export type LoginRequest = { type: string; params: unknown } export type ManageRequest = { type: string; params: unknown } export type ExternalLoginProvider = string export type LoginResponses = Record<string, unknown> export type ManageResponses = Record<string, unknown>}
declare module 'mogh_auth_client' { export * from 'npm:mogh_auth_client' export type LoginResponses = Record<string, unknown> export type ManageResponses = Record<string, unknown>}
declare module 'mogh_auth_client/dist/types.js' { export * from 'npm:mogh_auth_client'}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485import { ClientState, Types } from "./lib.js";import { ConnectTerminalQuery, ExecuteTerminalBody, InitTerminal } from "./types.js";export type TerminalCallbacks = { on_message?: (e: MessageEvent<any>) => void; on_login?: () => void; on_open?: () => void; on_close?: () => void;};export type ExecuteCallbacks = { onLine?: (line: string) => void | Promise<void>; onFinish?: (code: string) => void | Promise<void>;};export declare const terminal_methods: (url: string, state: ClientState) => { connect_terminal: ({ query: { target, terminal, init }, on_message, on_login, on_open, on_close, }: { query: ConnectTerminalQuery; } & TerminalCallbacks) => WebSocket; execute_terminal: (request: ExecuteTerminalBody, callbacks?: ExecuteCallbacks) => Promise<void>; execute_terminal_stream: (request: ExecuteTerminalBody) => Promise<AsyncIterable<string>>; execute_server_terminal: ({ server, terminal, command, init, }: { server: string; terminal?: string; command: string; init?: InitTerminal; }, callbacks?: ExecuteCallbacks) => Promise<void>; execute_container_terminal: ({ server, container, terminal, command, init, }: { server: string; container: string; terminal?: string; command: string; init?: InitTerminal; }, callbacks?: ExecuteCallbacks) => Promise<void>; execute_deployment_terminal: ({ deployment, terminal, command, init, }: { deployment: string; terminal?: string; command: string; init?: InitTerminal; }, callbacks?: ExecuteCallbacks) => Promise<void>; execute_stack_service_terminal: ({ stack, service, terminal, command, init, }: { stack: string; service: string; terminal?: string; command: string; init?: InitTerminal; }, callbacks?: ExecuteCallbacks) => Promise<void>; execute_container_exec: ({ server, container, shell, command, terminal, recreate, }: { /** Server Id or name */ server: string; /** The container name */ container: string; /** The shell to use (eg. `sh` or `bash`) */ shell: string; /** The command to execute. */ command: string; /** The name of the terminal to connect to */ terminal?: string; /** The behavior if */ recreate?: Types.TerminalRecreateMode; }, callbacks?: ExecuteCallbacks) => Promise<void>; execute_deployment_exec: ({ deployment, shell, command, terminal, recreate, }: { /** Deployment Id or name */ deployment: string; /** The shell to use (eg. `sh` or `bash`) */ shell: string; /** The command to execute. */ command: string; /** The name of the terminal to connect to */ terminal?: string; /** The behavior if */ recreate?: Types.TerminalRecreateMode; }, callbacks?: ExecuteCallbacks) => Promise<void>; execute_stack_exec: ({ stack, service, shell, command, terminal, recreate, }: { /** Stack Id or name */ stack: string; /** The service name to connect to */ service: string; /** The shell to use (eg. `sh` or `bash`) */ shell: string; /** The command to execute. */ command: string; /** The name of the terminal to connect to */ terminal?: string; /** The behavior if */ recreate?: Types.TerminalRecreateMode; }, callbacks?: ExecuteCallbacks) => Promise<void>;};