@amalgm/automations 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AXIOMS.md +32 -0
  2. package/PURPOSE.md +35 -0
  3. package/README.md +130 -0
  4. package/dist/src/automations.d.ts +27 -0
  5. package/dist/src/automations.js +199 -0
  6. package/dist/src/cli-main.d.ts +2 -0
  7. package/dist/src/cli-main.js +21 -0
  8. package/dist/src/cli.d.ts +10 -0
  9. package/dist/src/cli.js +183 -0
  10. package/dist/src/client.d.ts +7 -0
  11. package/dist/src/client.js +85 -0
  12. package/dist/src/contract.d.ts +176 -0
  13. package/dist/src/contract.js +1 -0
  14. package/dist/src/crud.d.ts +32 -0
  15. package/dist/src/crud.js +241 -0
  16. package/dist/src/errors.d.ts +17 -0
  17. package/dist/src/errors.js +32 -0
  18. package/dist/src/http.d.ts +7 -0
  19. package/dist/src/http.js +154 -0
  20. package/dist/src/index.d.ts +14 -0
  21. package/dist/src/index.js +18 -0
  22. package/dist/src/mcp-main.d.ts +2 -0
  23. package/dist/src/mcp-main.js +17 -0
  24. package/dist/src/mcp.d.ts +3 -0
  25. package/dist/src/mcp.js +88 -0
  26. package/dist/src/schedule.d.ts +2 -0
  27. package/dist/src/schedule.js +10 -0
  28. package/dist/src/schema.d.ts +236 -0
  29. package/dist/src/schema.js +119 -0
  30. package/dist/src/supabase-crud.d.ts +44 -0
  31. package/dist/src/supabase-crud.js +271 -0
  32. package/dist/src/supabase-store.d.ts +26 -0
  33. package/dist/src/supabase-store.js +123 -0
  34. package/dist/src/types.d.ts +128 -0
  35. package/dist/src/types.js +1 -0
  36. package/dist/src/validation.d.ts +11 -0
  37. package/dist/src/validation.js +69 -0
  38. package/dist/src/webhook.d.ts +18 -0
  39. package/dist/src/webhook.js +60 -0
  40. package/package.json +66 -0
  41. package/supabase/migrations/20260802000000_create_automations.sql +609 -0
package/AXIOMS.md ADDED
@@ -0,0 +1,32 @@
1
+ # Automations axioms
2
+
3
+ 1. Every automation record and CRUD decision has exactly one owner: this
4
+ product.
5
+ 2. The public control-plane contract is one auth-bound SDK. API, CLI, MCP, and
6
+ skill adapters translate inputs and call it; none accesses Supabase or owns
7
+ validation, authorization, or persistence rules.
8
+ 3. Every call is scoped by a resolved Amalgm principal. Public inputs never
9
+ contain a user id or raw credential; session, HMAC-refresh, and future API
10
+ key authentication all resolve to the same principal shape.
11
+ 4. An automation belongs to exactly one user and exactly one target. It may
12
+ have zero or more triggers and zero or one workflow.
13
+ 5. Scheduled triggers and webhook triggers are distinct first-class resources
14
+ with their own CRUD operations and validation rules.
15
+ 6. A workflow is the one script resource owned by its automation. It may be
16
+ created, read, updated, or deleted independently of the automation and its
17
+ triggers.
18
+ 7. An incomplete automation is valid configuration. Future execution behavior
19
+ must be derived from its persisted configuration rather than repaired by the
20
+ CRUD layer.
21
+ 8. Supabase is authoritative for automations, triggers, workflow source, and
22
+ permanent run history. A definition edit or deletion never rewrites prior
23
+ runs.
24
+ 9. Webhook secrets are persisted but write-only: normal reads reveal only that
25
+ a secret is configured.
26
+ 10. Run history is read-only in the control plane and always scoped to its
27
+ automation's owner.
28
+ 11. Storage records, Supabase RPCs, HTTP details, and MCP protocol details are
29
+ implementation concerns, not SDK concepts.
30
+ 12. During cutover, legacy and replacement implementations never write the
31
+ same authority concurrently. Completed cutover deletes the legacy
32
+ implementation.
package/PURPOSE.md ADDED
@@ -0,0 +1,35 @@
1
+ # Amalgm Automations
2
+
3
+ ## Purpose
4
+
5
+ Automations is the sole owner of Amalgm automation behavior and data. It lets an
6
+ authenticated user create, inspect, change, and delete automation
7
+ configuration — an automation belongs to one user and one target, may own any
8
+ number of scheduled and webhook triggers, and may own one workflow script — and
9
+ have each trigger occurrence run on the selected existing Amalgm machine.
10
+ Supabase holds everything except execution: the automation, its triggers, the
11
+ complete workflow, and permanent run history.
12
+
13
+ The product has two composable halves over that one state:
14
+
15
+ - **The configuration control plane** — one ergonomic, auth-bound SDK contract
16
+ for automation CRUD and run-history reads. The HTTP API, CLI, MCP server, and
17
+ skill are adapters over that contract; they do not access Supabase directly
18
+ or implement lifecycle rules.
19
+ - **The delivery rail** — trigger admission, scheduling, and run state: the
20
+ receipt-before-auth, lease, retry, and idempotency laws that decide when a
21
+ run happens. When a machine is offline, new runs remain pending; when it
22
+ reconnects, every pending run is sent through Amalgm's existing transport and
23
+ started by the existing host runtime. Execution itself stays on the machine.
24
+
25
+ Amalgm supplies a resolved authenticated principal from its user session or
26
+ HMAC-refresh flow; future API keys resolve to the same principal capability.
27
+ Core provides authenticated user identity, opaque machine identity,
28
+ connectivity, and narrow host capabilities. The product receives identity and
29
+ scopes — never raw credentials or Core storage — and neither side reaches into
30
+ the other's storage or reimplements the other's decisions.
31
+
32
+ This extraction is complete only when every Automations surface in Engine calls
33
+ this product service, product state has one writer, migrated behavior has parity
34
+ tests, and the legacy `amalgm-mcp` automation implementation and storage are
35
+ deleted.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # @amalgm/automations
2
+
3
+ The Automations product extracted from `amalgm-mcp`: a Supabase-backed
4
+ configuration control plane plus the delivery rail that decides when runs
5
+ happen. Supabase owns everything except execution.
6
+
7
+ ## SDK contract (configuration control plane)
8
+
9
+ The public control plane is an auth-bound SDK. API, CLI, MCP, and the
10
+ automation skill adapt this contract; only the service implementation accesses
11
+ Supabase.
12
+
13
+ ```ts
14
+ const sdk = service.for(principal);
15
+
16
+ await sdk.automations.create({ targetId: 'machine-1', name: 'Daily summary' });
17
+ await sdk.triggers.schedule.create('automation-id', { cron: '0 9 * * 1-5' });
18
+ await sdk.triggers.webhook.create('automation-id', {
19
+ source: 'github', event: 'push', secret: 'a-secret-at-least-16-characters',
20
+ });
21
+ await sdk.workflow.create('automation-id', {
22
+ script: 'export default workflow({ cells: [] });',
23
+ });
24
+ const history = await sdk.runs.list('automation-id');
25
+ ```
26
+
27
+ An automation may have any number of scheduled and webhook triggers, and zero
28
+ or one workflow. Every component can be created, read, changed, and deleted
29
+ independently. Webhook secrets are write-only; reads return `secretConfigured`.
30
+ Deleting configuration never deletes run history.
31
+
32
+ ### Composition
33
+
34
+ ```ts
35
+ import {
36
+ AutomationCrudService,
37
+ SupabaseAutomationCrudRepository,
38
+ createAutomationApi,
39
+ } from '@amalgm/automations';
40
+
41
+ const service = new AutomationCrudService(
42
+ new SupabaseAutomationCrudRepository(supabaseRpcClient),
43
+ );
44
+
45
+ const api = createAutomationApi({
46
+ service,
47
+ authenticate: resolveAmalgmPrincipal,
48
+ });
49
+ ```
50
+
51
+ `authenticate` resolves the existing Amalgm session or HMAC-refresh credential
52
+ to an `AutomationPrincipal`. Future API keys resolve to that same type. The
53
+ SDK receives a principal with scopes and never raw credentials or a caller-
54
+ supplied user ID.
55
+
56
+ Use `createAutomationClient` to call the Fetch API from another process. The
57
+ CLI and MCP server use this same client with
58
+ `AMALGM_AUTOMATIONS_API_URL` and `AMALGM_AUTOMATIONS_AUTHORIZATION`.
59
+
60
+ ```sh
61
+ amalgm-automations automations list
62
+ amalgm-automations triggers schedule create automation-id schedule.json
63
+ amalgm-automations triggers webhook create automation-id webhook.json
64
+ amalgm-automations workflow update automation-id workflow.json
65
+ ```
66
+
67
+ The MCP server command is `amalgm-automations-mcp`; it exposes one typed,
68
+ atomic MCP tool for every SDK operation.
69
+
70
+ ## Delivery rail
71
+
72
+ The Core boundary is deliberately small. Core translates its authenticated
73
+ machine record into an opaque `AutomationTarget`; the product never queries a
74
+ Core table. Core also supplies the existing machine transport:
75
+
76
+ ```ts
77
+ import { Automations, SupabaseStore } from '@amalgm/automations';
78
+
79
+ const automations = new Automations(new SupabaseStore(getSupabase()), {
80
+ isOnline: ({ targetId }) => existingTunnel.hasConnection(targetId),
81
+ send: (run) => existingTunnel.send(run.targetId, run),
82
+ });
83
+
84
+ await automations.save(authenticatedUser.id, definition);
85
+
86
+ await automations.receiveEvent({
87
+ target: await core.resolveEventTarget(eventRef),
88
+ headers,
89
+ body,
90
+ payload,
91
+ });
92
+
93
+ await automations.targetOnline({ userId, targetId });
94
+
95
+ await automations.updateRun({ userId, targetId }, runId, {
96
+ status: 'completed',
97
+ output,
98
+ });
99
+
100
+ const history = await automations.listRuns(userId, automationId);
101
+ ```
102
+
103
+ `save`, `receiveEvent`, `fireDueCrons`, `targetOnline`, `updateRun`, and
104
+ `listRuns` own the decisions.
105
+ The surrounding Engine route, MCP tool, scheduler callback, and tunnel callback
106
+ only translate and call them.
107
+
108
+ Every firing stores an immutable run snapshot in Supabase. Delivery and machine
109
+ execution update that record instead of deleting it. When a machine is offline,
110
+ new runs remain pending; on reconnect every pending run drains through the
111
+ existing transport.
112
+
113
+ The current implementation covers Supabase definitions, cron and event
114
+ admission, secret verification, permanent run history, and reconnect drain. The
115
+ remaining extraction is the proven workflow definition, validation,
116
+ orchestration, and public surface behavior still owned by `amalgm-mcp`; it must
117
+ move behind this same service boundary before legacy code is removed.
118
+
119
+ The purpose is in [PURPOSE.md](./PURPOSE.md), and the non-negotiable ownership
120
+ rules are in [AXIOMS.md](./AXIOMS.md).
121
+
122
+ ## Verification
123
+
124
+ ```sh
125
+ npm install
126
+ npm run release:check
127
+ ```
128
+
129
+ `release:check` includes deterministic unit tests and a real Postgres contract.
130
+ Set `TEST_DATABASE_URL` to an empty Postgres 16 database for the latter.
@@ -0,0 +1,27 @@
1
+ import type { AutomationDefinition, AutomationLog, AutomationRun, AutomationStore, AutomationTarget, AutomationTransport, Json, RunUpdate } from './types.js';
2
+ export declare class EventRejectedError extends Error {
3
+ constructor();
4
+ }
5
+ export declare class Automations {
6
+ #private;
7
+ readonly store: AutomationStore;
8
+ readonly transport: AutomationTransport;
9
+ readonly log: AutomationLog;
10
+ constructor(store: AutomationStore, transport: AutomationTransport, log?: AutomationLog);
11
+ save(userId: string, definition: AutomationDefinition, now?: Date): Promise<void>;
12
+ delete(userId: string, automationId: string): Promise<boolean>;
13
+ listRuns(userId: string, automationId?: string): Promise<AutomationRun[]>;
14
+ receiveEvent(input: {
15
+ target: AutomationTarget;
16
+ headers: Record<string, string>;
17
+ body: Buffer;
18
+ payload: Json;
19
+ source?: string;
20
+ event?: string;
21
+ now?: Date;
22
+ }): Promise<AutomationRun[]>;
23
+ fireDueCrons(now?: Date): Promise<AutomationRun[]>;
24
+ targetOnline(target: AutomationTarget): Promise<number>;
25
+ updateRun(target: AutomationTarget, runId: string, update: RunUpdate): Promise<AutomationRun | null>;
26
+ drain(target: AutomationTarget): Promise<number>;
27
+ }
@@ -0,0 +1,199 @@
1
+ import { nextCronAt, validateCron } from './schedule.js';
2
+ import { eventReferences, matchesEvent, verifyEventSecret } from './webhook.js';
3
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/;
4
+ const noop = () => { };
5
+ export class EventRejectedError extends Error {
6
+ constructor() {
7
+ super('Event was not authenticated');
8
+ this.name = 'EventRejectedError';
9
+ }
10
+ }
11
+ export class Automations {
12
+ store;
13
+ transport;
14
+ log;
15
+ #drains = new Map();
16
+ constructor(store, transport, log = noop) {
17
+ this.store = store;
18
+ this.transport = transport;
19
+ this.log = log;
20
+ }
21
+ async save(userId, definition, now = new Date()) {
22
+ const automation = prepareAutomation(userId, definition, now);
23
+ await this.store.saveAutomation(automation);
24
+ }
25
+ delete(userId, automationId) {
26
+ return this.store.deleteAutomation(userId, automationId);
27
+ }
28
+ listRuns(userId, automationId) {
29
+ return this.store.listRuns(userId, automationId);
30
+ }
31
+ async receiveEvent(input) {
32
+ assertJson(input.payload, 'Event payload');
33
+ const candidates = await this.store.eventTriggers(input.target);
34
+ const authenticated = candidates.filter((trigger) => (verifyEventSecret(trigger.secret, input.headers, input.body)));
35
+ if (authenticated.length === 0) {
36
+ this.log('event.rejected', { targetId: input.target.targetId });
37
+ throw new EventRejectedError();
38
+ }
39
+ const references = input.source && input.event
40
+ ? { primary: { source: input.source, event: input.event } }
41
+ : eventReferences(input.headers, input.payload);
42
+ let reference = references.primary;
43
+ let triggers = authenticated.filter((trigger) => matchesEvent(trigger, reference.source, reference.event));
44
+ if (triggers.length === 0 && references.fallback) {
45
+ reference = references.fallback;
46
+ triggers = authenticated.filter((trigger) => matchesEvent(trigger, reference.source, reference.event));
47
+ }
48
+ if (triggers.length === 0)
49
+ return [];
50
+ const now = input.now || new Date();
51
+ const runInput = { kind: 'event', ...reference, payload: input.payload };
52
+ const runs = (await Promise.all(triggers.map((trigger) => (this.store.enqueueEvent(trigger, runInput, now))))).filter((run) => run !== null);
53
+ await this.#drainCreated(runs);
54
+ return runs;
55
+ }
56
+ async fireDueCrons(now = new Date()) {
57
+ const created = [];
58
+ for (const trigger of await this.store.dueCronTriggers(now)) {
59
+ let scheduledFor = trigger.nextRunAt;
60
+ while (new Date(scheduledFor) <= now) {
61
+ const nextRunAt = nextCronAt(trigger.cron, trigger.timezone, scheduledFor);
62
+ const run = await this.store.enqueueCron({ ...trigger, nextRunAt: scheduledFor }, nextRunAt, now);
63
+ if (!run)
64
+ break;
65
+ created.push(run);
66
+ scheduledFor = nextRunAt;
67
+ }
68
+ }
69
+ await this.#drainCreated(created);
70
+ return created;
71
+ }
72
+ targetOnline(target) {
73
+ return this.drain(target);
74
+ }
75
+ updateRun(target, runId, update) {
76
+ if (update.output !== undefined)
77
+ assertJson(update.output, 'Run output');
78
+ return this.store.updateRun(target, runId, update);
79
+ }
80
+ drain(target) {
81
+ const key = `${target.userId}:${target.targetId}`;
82
+ const active = this.#drains.get(key);
83
+ if (active)
84
+ return active;
85
+ const drain = this.#drain(target).finally(() => {
86
+ if (this.#drains.get(key) === drain)
87
+ this.#drains.delete(key);
88
+ });
89
+ this.#drains.set(key, drain);
90
+ return drain;
91
+ }
92
+ async #drainCreated(runs) {
93
+ for (const run of runs) {
94
+ this.log('run.pending', {
95
+ runId: run.id,
96
+ targetId: run.targetId,
97
+ automationId: run.automationId,
98
+ });
99
+ }
100
+ const targets = new Map(runs.map((run) => [
101
+ `${run.userId}:${run.targetId}`,
102
+ { userId: run.userId, targetId: run.targetId },
103
+ ]));
104
+ await Promise.all([...targets.values()].map((target) => this.drain(target)));
105
+ }
106
+ async #drain(target) {
107
+ if (!this.transport.isOnline(target))
108
+ return 0;
109
+ this.log('drain.started', { targetId: target.targetId });
110
+ let sent = 0;
111
+ const failed = new Set();
112
+ while (this.transport.isOnline(target)) {
113
+ const runs = (await this.store.pendingRuns(target)).filter(({ id }) => !failed.has(id));
114
+ if (runs.length === 0)
115
+ return sent;
116
+ for (const run of runs) {
117
+ if (!this.transport.isOnline(target))
118
+ return sent;
119
+ try {
120
+ if (!await this.transport.send(run)) {
121
+ this.log('run.failed', { runId: run.id, targetId: target.targetId });
122
+ failed.add(run.id);
123
+ continue;
124
+ }
125
+ if (await this.store.markSent(run.id, target, new Date()))
126
+ sent += 1;
127
+ this.log('run.sent', { runId: run.id, targetId: target.targetId });
128
+ }
129
+ catch {
130
+ this.log('run.failed', { runId: run.id, targetId: target.targetId });
131
+ failed.add(run.id);
132
+ }
133
+ }
134
+ }
135
+ return sent;
136
+ }
137
+ }
138
+ function prepareAutomation(userId, definition, now) {
139
+ if (!ID.test(definition.id))
140
+ throw new Error('Automation id is invalid');
141
+ if (!userId)
142
+ throw new Error('Automation userId is required');
143
+ if (!definition.targetId)
144
+ throw new Error('Automation targetId is required');
145
+ if (!ID.test(definition.trigger.id))
146
+ throw new Error('Trigger id is invalid');
147
+ if (!ID.test(definition.workflow.id))
148
+ throw new Error('Workflow id is invalid');
149
+ if (!definition.workflow.script.trim())
150
+ throw new Error('Workflow script is required');
151
+ if (definition.workflow.compiled !== undefined) {
152
+ assertJson(definition.workflow.compiled, 'Compiled workflow');
153
+ }
154
+ if (definition.workflow.allowlist !== undefined) {
155
+ assertJson(definition.workflow.allowlist, 'Workflow allowlist');
156
+ }
157
+ if (definition.workflow.limits !== undefined) {
158
+ assertJson(definition.workflow.limits, 'Workflow limits');
159
+ }
160
+ const trigger = definition.trigger.kind === 'event'
161
+ ? prepareEventTrigger(definition.trigger)
162
+ : prepareCronTrigger(definition.trigger, now);
163
+ return { ...definition, userId, enabled: definition.enabled !== false, trigger };
164
+ }
165
+ function prepareEventTrigger(trigger) {
166
+ if (trigger.secret.length < 16)
167
+ throw new Error(`Event trigger ${trigger.id} secret is too short`);
168
+ return {
169
+ ...trigger,
170
+ enabled: trigger.enabled !== false,
171
+ source: trigger.source || '*',
172
+ event: trigger.event || '*',
173
+ };
174
+ }
175
+ function prepareCronTrigger(trigger, now) {
176
+ const timezone = trigger.timezone || 'UTC';
177
+ validateCron(trigger.cron, timezone);
178
+ return {
179
+ ...trigger,
180
+ enabled: trigger.enabled !== false,
181
+ timezone,
182
+ nextRunAt: nextCronAt(trigger.cron, timezone, now),
183
+ };
184
+ }
185
+ function assertJson(value, label) {
186
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
187
+ return;
188
+ if (typeof value === 'number' && Number.isFinite(value))
189
+ return;
190
+ if (Array.isArray(value)) {
191
+ value.forEach((item) => assertJson(item, label));
192
+ return;
193
+ }
194
+ if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
195
+ Object.values(value).forEach((item) => assertJson(item, label));
196
+ return;
197
+ }
198
+ throw new Error(`${label} must be JSON`);
199
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { createAutomationClient } from './client.js';
3
+ import { automationCliHelp, runAutomationCli } from './cli.js';
4
+ const baseUrl = process.env.AMALGM_AUTOMATIONS_API_URL;
5
+ const authorization = process.env.AMALGM_AUTOMATIONS_AUTHORIZATION;
6
+ const argv = process.argv.slice(2);
7
+ const help = argv.length === 0 || argv[0] === 'help' || argv[0] === '--help' || argv[0] === '-h';
8
+ if (help) {
9
+ process.stdout.write(automationCliHelp);
10
+ }
11
+ else if (!baseUrl || !authorization) {
12
+ process.stderr.write('AMALGM_AUTOMATIONS_API_URL and AMALGM_AUTOMATIONS_AUTHORIZATION are required.\n');
13
+ process.exitCode = 1;
14
+ }
15
+ else {
16
+ const code = await runAutomationCli(argv, createAutomationClient({
17
+ baseUrl,
18
+ authorization: () => authorization,
19
+ }));
20
+ process.exitCode = code;
21
+ }
@@ -0,0 +1,10 @@
1
+ import type { AutomationCrud } from './contract.js';
2
+ interface Output {
3
+ write(chunk: string): unknown;
4
+ }
5
+ export declare const automationCliHelp = "Usage: amalgm-automations <resource> <action>\n\nautomations create|list|get|update|delete\ntriggers list | schedule <create|list|get|update|delete> | webhook <create|list|get|update|delete>\nworkflow create|get|update|delete\nruns list|get\n\nList filters:\n automations list [--target-id ID] [--enabled true|false] [--limit N] [--offset N]\n triggers schedule|webhook list AUTOMATION_ID [--limit N] [--offset N]\n runs list AUTOMATION_ID [--status STATUS] [--limit N] [--offset N]\n";
6
+ export declare function runAutomationCli(argv: string[], sdk: AutomationCrud, output?: {
7
+ stdout?: Output;
8
+ stderr?: Output;
9
+ }): Promise<number>;
10
+ export {};
@@ -0,0 +1,183 @@
1
+ import fs from 'node:fs/promises';
2
+ export const automationCliHelp = `Usage: amalgm-automations <resource> <action>
3
+
4
+ automations create|list|get|update|delete
5
+ triggers list | schedule <create|list|get|update|delete> | webhook <create|list|get|update|delete>
6
+ workflow create|get|update|delete
7
+ runs list|get
8
+
9
+ List filters:
10
+ automations list [--target-id ID] [--enabled true|false] [--limit N] [--offset N]
11
+ triggers schedule|webhook list AUTOMATION_ID [--limit N] [--offset N]
12
+ runs list AUTOMATION_ID [--status STATUS] [--limit N] [--offset N]
13
+ `;
14
+ export async function runAutomationCli(argv, sdk, output = {}) {
15
+ const stdout = output.stdout || process.stdout;
16
+ const stderr = output.stderr || process.stderr;
17
+ try {
18
+ const [resource, action, ...rest] = argv;
19
+ if (!resource || resource === 'help' || resource === '--help' || resource === '-h') {
20
+ stdout.write(automationCliHelp);
21
+ return 0;
22
+ }
23
+ const result = await command(resource, action, rest, sdk);
24
+ stdout.write(`${JSON.stringify(result, null, 2)}\n`);
25
+ return 0;
26
+ }
27
+ catch (error) {
28
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
29
+ return 1;
30
+ }
31
+ }
32
+ async function command(resource, action, args, sdk) {
33
+ if (resource === 'automations')
34
+ return automationCommand(action, args, sdk);
35
+ if (resource === 'triggers')
36
+ return triggerCommand(action, args, sdk);
37
+ if (resource === 'workflow')
38
+ return workflowCommand(action, args, sdk);
39
+ if (resource === 'runs')
40
+ return runsCommand(action, args, sdk);
41
+ throw new Error(`Unknown resource: ${resource}`);
42
+ }
43
+ async function automationCommand(action, args, sdk) {
44
+ if (action === 'create')
45
+ return sdk.automations.create(await jsonFile(required(args, 0, 'automation JSON file')));
46
+ if (action === 'list')
47
+ return sdk.automations.list(automationListOptions(args));
48
+ if (action === 'get')
49
+ return sdk.automations.get(required(args, 0, 'automation id'));
50
+ if (action === 'update')
51
+ return sdk.automations.update(required(args, 0, 'automation id'), await jsonFile(required(args, 1, 'automation patch JSON file')));
52
+ if (action === 'delete') {
53
+ const automationId = required(args, 0, 'automation id');
54
+ await sdk.automations.delete(automationId);
55
+ return { deleted: automationId };
56
+ }
57
+ throw new Error('Usage: automations <create|list|get|update|delete>');
58
+ }
59
+ async function triggerCommand(action, args, sdk) {
60
+ if (action === 'list')
61
+ return sdk.triggers.list(required(args, 0, 'automation id'));
62
+ const [operation, automationId, ...rest] = args;
63
+ const automation = requiredValue(automationId, 'automation id');
64
+ if (action === 'schedule') {
65
+ if (operation === 'create')
66
+ return sdk.triggers.schedule.create(automation, await jsonFile(required(rest, 0, 'schedule JSON file')));
67
+ if (operation === 'list')
68
+ return sdk.triggers.schedule.list(automation, pageOptions(rest));
69
+ if (operation === 'get')
70
+ return sdk.triggers.schedule.get(automation, required(rest, 0, 'schedule trigger id'));
71
+ if (operation === 'update')
72
+ return sdk.triggers.schedule.update(automation, required(rest, 0, 'schedule trigger id'), await jsonFile(required(rest, 1, 'schedule patch JSON file')));
73
+ if (operation === 'delete') {
74
+ const id = required(rest, 0, 'schedule trigger id');
75
+ await sdk.triggers.schedule.delete(automation, id);
76
+ return { deleted: id };
77
+ }
78
+ }
79
+ if (action === 'webhook') {
80
+ if (operation === 'create')
81
+ return sdk.triggers.webhook.create(automation, await jsonFile(required(rest, 0, 'webhook JSON file')));
82
+ if (operation === 'list')
83
+ return sdk.triggers.webhook.list(automation, pageOptions(rest));
84
+ if (operation === 'get')
85
+ return sdk.triggers.webhook.get(automation, required(rest, 0, 'webhook trigger id'));
86
+ if (operation === 'update')
87
+ return sdk.triggers.webhook.update(automation, required(rest, 0, 'webhook trigger id'), await jsonFile(required(rest, 1, 'webhook patch JSON file')));
88
+ if (operation === 'delete') {
89
+ const id = required(rest, 0, 'webhook trigger id');
90
+ await sdk.triggers.webhook.delete(automation, id);
91
+ return { deleted: id };
92
+ }
93
+ }
94
+ throw new Error('Usage: triggers <list|schedule|webhook>');
95
+ }
96
+ async function workflowCommand(action, args, sdk) {
97
+ const automationId = required(args, 0, 'automation id');
98
+ if (action === 'create')
99
+ return sdk.workflow.create(automationId, await jsonFile(required(args, 1, 'workflow JSON file')));
100
+ if (action === 'get')
101
+ return sdk.workflow.get(automationId);
102
+ if (action === 'update')
103
+ return sdk.workflow.update(automationId, await jsonFile(required(args, 1, 'workflow patch JSON file')));
104
+ if (action === 'delete') {
105
+ await sdk.workflow.delete(automationId);
106
+ return { deleted: automationId };
107
+ }
108
+ throw new Error('Usage: workflow <create|get|update|delete>');
109
+ }
110
+ async function runsCommand(action, args, sdk) {
111
+ const automationId = required(args, 0, 'automation id');
112
+ if (action === 'list')
113
+ return sdk.runs.list(automationId, runListOptions(args.slice(1)));
114
+ if (action === 'get')
115
+ return sdk.runs.get(automationId, required(args, 1, 'run id'));
116
+ throw new Error('Usage: runs <list|get>');
117
+ }
118
+ function automationListOptions(args) {
119
+ const options = readOptions(args, new Set(['--target-id', '--enabled', '--limit', '--offset']));
120
+ return {
121
+ ...pageOptionsFrom(options),
122
+ ...(options['--target-id'] ? { targetId: options['--target-id'] } : {}),
123
+ ...(options['--enabled'] === undefined ? {} : { enabled: booleanOption(options['--enabled'], '--enabled') }),
124
+ };
125
+ }
126
+ function runListOptions(args) {
127
+ const options = readOptions(args, new Set(['--status', '--limit', '--offset']));
128
+ return {
129
+ ...pageOptionsFrom(options),
130
+ ...(options['--status'] ? { status: options['--status'] } : {}),
131
+ };
132
+ }
133
+ function pageOptions(args) {
134
+ return pageOptionsFrom(readOptions(args, new Set(['--limit', '--offset'])));
135
+ }
136
+ function pageOptionsFrom(options) {
137
+ return {
138
+ ...(options['--limit'] === undefined ? {} : { limit: numberOption(options['--limit'], '--limit') }),
139
+ ...(options['--offset'] === undefined ? {} : { offset: numberOption(options['--offset'], '--offset') }),
140
+ };
141
+ }
142
+ function readOptions(args, allowed) {
143
+ const result = {};
144
+ for (let index = 0; index < args.length; index += 2) {
145
+ const flag = args[index];
146
+ const value = args[index + 1];
147
+ if (!flag || !allowed.has(flag) || value === undefined)
148
+ throw new Error(`Unknown or incomplete list option: ${flag || ''}`);
149
+ if (result[flag] !== undefined)
150
+ throw new Error(`List option may be provided once: ${flag}`);
151
+ result[flag] = value;
152
+ }
153
+ return result;
154
+ }
155
+ function numberOption(value, name) {
156
+ const number = Number(value);
157
+ if (!Number.isInteger(number))
158
+ throw new Error(`${name} must be an integer`);
159
+ return number;
160
+ }
161
+ function booleanOption(value, name) {
162
+ if (value === 'true')
163
+ return true;
164
+ if (value === 'false')
165
+ return false;
166
+ throw new Error(`${name} must be true or false`);
167
+ }
168
+ async function jsonFile(filename) {
169
+ try {
170
+ return JSON.parse(await fs.readFile(filename, 'utf8'));
171
+ }
172
+ catch (error) {
173
+ throw new Error(`Cannot read JSON file ${filename}: ${error instanceof Error ? error.message : String(error)}`);
174
+ }
175
+ }
176
+ function required(values, index, label) {
177
+ return requiredValue(values[index], label);
178
+ }
179
+ function requiredValue(value, label) {
180
+ if (!value)
181
+ throw new Error(`${label} is required`);
182
+ return value;
183
+ }
@@ -0,0 +1,7 @@
1
+ import type { AutomationCrud } from './contract.js';
2
+ export type AutomationAuthorization = () => string | undefined | Promise<string | undefined>;
3
+ export declare function createAutomationClient(options: {
4
+ baseUrl: string;
5
+ authorization: AutomationAuthorization;
6
+ fetch?: typeof globalThis.fetch;
7
+ }): AutomationCrud;