import {pathToFileURL} from 'node:url'; function required(value, name) { if (!value) throw new Error(`${name} is required`); return value; } function positiveInteger(value, name) { if (!Number.isInteger(value) || value < 1) throw new Error(`${name} must be a positive integer`); return value; } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } export class Machine10Client { constructor({ appId, bearer, grant = null, gateway = 'https://jmpkit.com/api', fetchImpl = globalThis.fetch }) { this.appId = required(appId, 'appId'); this.bearer = required(bearer, 'bearer'); this.grant = grant; this.fetch = required(fetchImpl, 'fetchImpl'); this.appBase = `${gateway.replace(/\/$/, '')}/apps/${encodeURIComponent(appId)}`; this.runUrl = `${this.appBase}/machine10/run`; this.base = `${this.appBase}/machine10/main`; this.originFallbackUrl = `${this.appBase}/origin10/fallback`; } async requestUrl(url, method, body) { const headers = {authorization: `Bearer ${this.bearer}`}; if (this.grant) headers['x-jmpkit-grant'] = this.grant; if (body !== undefined) headers['content-type'] = 'application/json'; const response = await this.fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) }); const text = await response.text(); let value; try { value = text ? JSON.parse(text) : null; } catch { throw new Error(`Machine10 returned ${response.status} with non-JSON body`); } if (!response.ok) { const error = new Error(value?.message || value?.error || `Machine10 returned ${response.status}`); error.status = response.status; error.code = value?.code || null; error.response = value; throw error; } return value; } request(method, suffix = '', body) { return this.requestUrl(`${this.base}${suffix}`, method, body); } run(body) { return this.requestUrl(this.runUrl, 'POST', body); } inspect() { return this.request('GET'); } install(body) { return this.request('POST', '', body); } delete(confirmAppId = this.appId) { return this.request('DELETE', '', {confirmAppId}); } replaceProgram(body) { return this.request('PUT', '/program', body); } rollback(toProgramRevision) { return this.request('POST', '/program/rollback', {toProgramRevision}); } trigger(body) { return this.request('POST', '/events', body); } schedule(body) { return this.request('PUT', '/timer', body); } cancelTimer() { return this.request('DELETE', '/timer'); } pause() { return this.request('POST', '/pause'); } resume() { return this.request('POST', '/resume'); } stop() { return this.request('POST', '/stop'); } retry() { return this.request('POST', '/retry'); } enableOriginFallback() { return this.requestUrl(this.originFallbackUrl, 'PUT'); } disableOriginFallback() { return this.requestUrl(this.originFallbackUrl, 'DELETE'); } generation(number) { if (!Number.isSafeInteger(number) || number < 0) throw new Error('generation must be a non-negative integer'); return this.request('GET', `/generations/${number}`); } async waitFor(predicate, {timeoutMs = 20_000, intervalMs = 100} = {}) { positiveInteger(timeoutMs, 'timeoutMs'); positiveInteger(intervalMs, 'intervalMs'); const deadline = Date.now() + timeoutMs; for (;;) { const value = await this.inspect(); if (await predicate(value.machine)) return value.machine; if (Date.now() >= deadline) throw new Error('Machine10 wait timed out'); await delay(Math.min(intervalMs, Math.max(1, deadline - Date.now()))); } } waitForGeneration(generation, options) { if (!Number.isSafeInteger(generation) || generation < 0) { throw new Error('generation must be a non-negative integer'); } return this.waitFor( (machine) => machine.generation >= generation && ['waiting', 'idle', 'paused', 'stopped', 'failed'].includes(machine.status), options ); } waitForOccurrence(occurrenceId, options) { required(occurrenceId, 'occurrenceId'); return this.waitFor(async (machine) => { if (machine.lastTurn?.occurrenceId === occurrenceId) return true; const committed = machine.generationHistory?.find((entry) => entry.occurrenceId === occurrenceId); if (committed) return true; if (machine.deadLetters?.some((entry) => entry.occurrenceId === occurrenceId)) { throw new Error(`Machine10 occurrence expired: ${occurrenceId}`); } if (machine.failureBlocked && ( machine.inflight?.event?.occurrenceId === occurrenceId || machine.queue?.some((entry) => entry.occurrenceId === occurrenceId) )) { throw new Error(machine.lastError?.message || `Machine10 occurrence failed: ${occurrenceId}`); } return false; }, options); } } export function clientFromEnv(env = process.env) { return new Machine10Client({ appId: env.JMPKIT_APP_ID, bearer: env.JMPKIT_BEARER, grant: env.JMPKIT_GRANT || null, gateway: env.JMPKIT_API || 'https://jmpkit.com/api' }); } async function main() { const [action = 'inspect', json = null] = process.argv.slice(2); const client = clientFromEnv(); const body = json ? JSON.parse(json) : undefined; const operations = { inspect: () => client.inspect(), run: () => client.run(required(body, 'run JSON')), install: () => client.install(required(body, 'install JSON')), event: () => client.trigger(required(body, 'event JSON')), timer: () => client.schedule(required(body, 'timer JSON')), pause: () => client.pause(), resume: () => client.resume(), stop: () => client.stop(), retry: () => client.retry(), 'cancel-timer': () => client.cancelTimer(), 'origin-on': () => client.enableOriginFallback(), 'origin-off': () => client.disableOriginFallback() }; if (!operations[action]) throw new Error(`unknown action: ${action}`); process.stdout.write(`${JSON.stringify(await operations[action](), null, 2)}\n`); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main().catch((error) => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }); }