@frontera-sdk/cli 1.43.6 → 1.43.7
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.
- package/package.json +3 -3
- package/src/api/automation-api.ts +86 -6
- package/src/api/blueprint-authoring-api.ts +38 -0
- package/src/automation-template.ts +13 -3
- package/src/blueprint/compile.ts +168 -2
- package/src/blueprint/diff.ts +132 -2
- package/src/blueprint/model.ts +48 -13
- package/src/blueprint/projection.ts +217 -21
- package/src/blueprint/render.ts +24 -1
- package/src/blueprint/scaffold.ts +149 -13
- package/src/commands/automation/build-entry.ts +73 -0
- package/src/commands/automation/dev.ts +453 -0
- package/src/commands/automation/index-commands.ts +5 -55
- package/src/commands/automation/run.ts +127 -10
- package/src/commands/blueprint/declarative.ts +480 -37
- package/src/flag-help.ts +9 -0
- package/src/vendor/sdk-sources.json +10 -9
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type AutomationRunSummary,
|
|
6
6
|
type RegistryStatus,
|
|
7
7
|
} from '../../api/automation-api'
|
|
8
|
-
import { flagBool, type Command, type CommandContext } from '../types'
|
|
8
|
+
import { flagBool, flagString, type Command, type CommandContext } from '../types'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* How long to wait for a queued run to reach a terminal state.
|
|
@@ -114,9 +114,68 @@ export function describeTrail(steps: AutomationRunStep[]): string[] {
|
|
|
114
114
|
return lines
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* `--version N`, or undefined for the live one.
|
|
119
|
+
*
|
|
120
|
+
* Exported so the parsing is testable without a network client. Rejects here
|
|
121
|
+
* rather than forwarding a bad value: `--version abc` sent onward arrives as a
|
|
122
|
+
* schema violation naming `version` as an integer, which is true and useless —
|
|
123
|
+
* the author typed a word, and the message should say so.
|
|
124
|
+
*/
|
|
125
|
+
export function parseVersionFlag(ctx: CommandContext): number | undefined {
|
|
126
|
+
const raw = flagString(ctx, 'version')
|
|
127
|
+
if (raw === undefined) return undefined
|
|
128
|
+
const parsed = Number(raw)
|
|
129
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
130
|
+
throw new UsageError(
|
|
131
|
+
`"${raw}" is not a version number`,
|
|
132
|
+
'versions are whole numbers from 1 — `frontera automation versions <slug>` lists them',
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
return parsed
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Wait for the run this invoke started, and read its trail.
|
|
141
|
+
*
|
|
142
|
+
* Shared by the live and dev paths because the question is the same one — the
|
|
143
|
+
* only difference is how the run was started. Identified by id against a
|
|
144
|
+
* baseline taken BEFORE the invoke: counting rows would claim a concurrently
|
|
145
|
+
* firing cron's run as this one, which on a five-minute schedule is not a rare
|
|
146
|
+
* race.
|
|
147
|
+
*/
|
|
148
|
+
async function waitForRun(
|
|
149
|
+
client: AutomationApi,
|
|
150
|
+
slug: string,
|
|
151
|
+
priorId: string | null,
|
|
152
|
+
mode: 'live' | 'dev' = 'live',
|
|
153
|
+
): Promise<{ run: AutomationRunSummary | undefined; steps: AutomationRunStep[] }> {
|
|
154
|
+
const deadline = Date.now() + WAIT_TIMEOUT_MS
|
|
155
|
+
let seen: AutomationRunSummary | undefined
|
|
156
|
+
while (Date.now() < deadline) {
|
|
157
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS))
|
|
158
|
+
const [latest] = await client.runs(slug, 1, mode)
|
|
159
|
+
if (isNewRun(priorId, latest) && latest) {
|
|
160
|
+
seen = latest
|
|
161
|
+
if (isTerminal(latest.status)) {
|
|
162
|
+
// Best-effort: a run that finished is the answer, and failing to read its
|
|
163
|
+
// trail must not turn a successful run into a failed command.
|
|
164
|
+
const steps = await client.runSteps(latest.id).catch(() => [])
|
|
165
|
+
return { run: latest, steps }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { run: seen, steps: [] }
|
|
170
|
+
}
|
|
171
|
+
|
|
117
172
|
/** One line describing a finished run, its trail, and its output. */
|
|
118
173
|
export function describeRun(run: AutomationRunSummary, steps: AutomationRunStep[] = []): string {
|
|
119
|
-
|
|
174
|
+
// A dev run has no version — its code never left the author's machine — and
|
|
175
|
+
// `v${null}` printed `vnull` on the first line the author reads. Named rather
|
|
176
|
+
// than dashed here, because the CLI has room for the reason.
|
|
177
|
+
const where = run.version === null ? 'from a dev session' : `v${run.version}`
|
|
178
|
+
const lines = [`${run.status} — ${where}, ${run.ctxCalls} ctx call(s)`]
|
|
120
179
|
if (run.errorMessage) lines.push(` ${run.errorMessage}`)
|
|
121
180
|
// The trail is why an author ran this by hand: which steps executed, in what
|
|
122
181
|
// order, and where the time went. Without it the CLI reported an outcome and
|
|
@@ -147,10 +206,12 @@ export const automationRun: Command = {
|
|
|
147
206
|
noun: 'automation',
|
|
148
207
|
verb: 'run',
|
|
149
208
|
args: [{ name: 'slug', required: true, description: 'automation slug' }],
|
|
150
|
-
flags: { 'no-wait': 'boolean' },
|
|
151
|
-
summary: 'Run the live version now and report what it did',
|
|
209
|
+
flags: { 'no-wait': 'boolean', version: 'string', dev: 'boolean' },
|
|
210
|
+
summary: 'Run the live version now — or a named one, or the dev session — and report what it did',
|
|
152
211
|
examples: [
|
|
153
212
|
'frontera automation run daily-digest',
|
|
213
|
+
'frontera automation run daily-digest --dev',
|
|
214
|
+
'frontera automation run daily-digest --version 7',
|
|
154
215
|
'frontera automation run daily-digest --no-wait',
|
|
155
216
|
],
|
|
156
217
|
},
|
|
@@ -158,6 +219,46 @@ export const automationRun: Command = {
|
|
|
158
219
|
async run(ctx) {
|
|
159
220
|
const slug = requireSlug(ctx)
|
|
160
221
|
const client = new AutomationApi(ctx.apiUrl, ctx.token)
|
|
222
|
+
// Parsed here rather than passed through as a string: a typo should fail
|
|
223
|
+
// before the registry wait, not after it, and `--version abc` reaching the
|
|
224
|
+
// service as `NaN` would come back as a schema error naming a field the
|
|
225
|
+
// author never typed.
|
|
226
|
+
const version = parseVersionFlag(ctx)
|
|
227
|
+
const dev = flagBool(ctx, 'dev')
|
|
228
|
+
if (dev && version !== undefined) {
|
|
229
|
+
throw new UsageError(
|
|
230
|
+
'--dev and --version name different things to run',
|
|
231
|
+
'a dev run executes the file on the machine holding the session; it has no version',
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// A dev run never reaches the broker, so the registry wait below is not just
|
|
236
|
+
// unnecessary — it would refuse a perfectly good dev run in an environment
|
|
237
|
+
// that has no runner deployed at all, which is exactly where a dev loop is
|
|
238
|
+
// most useful.
|
|
239
|
+
if (dev) {
|
|
240
|
+
// `'dev'` on BOTH the baseline and the wait. Without it each reads live
|
|
241
|
+
// history: the command sits out its whole timeout for a run that finished
|
|
242
|
+
// seconds in, and a cron fire during the wait is reported as if it were
|
|
243
|
+
// the dev run.
|
|
244
|
+
const before = await client.runs(slug, 1, 'dev')
|
|
245
|
+
const priorId = before[0]?.id ?? null
|
|
246
|
+
const queued = await client.run(slug, undefined, true)
|
|
247
|
+
if (flagBool(ctx, 'no-wait')) {
|
|
248
|
+
return {
|
|
249
|
+
data: queued,
|
|
250
|
+
text: `Queued a dev run of ${slug}.\n`
|
|
251
|
+
+ ` frontera automation runs ${slug} # to see how it went`,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const seen = await waitForRun(client, slug, priorId, 'dev')
|
|
255
|
+
return {
|
|
256
|
+
data: seen.run ? { ...seen.run, steps: seen.steps } : queued,
|
|
257
|
+
text: seen.run
|
|
258
|
+
? `${slug}: ${describeRun(seen.run, seen.steps)}`
|
|
259
|
+
: `${slug}: the dev run did not finish within ${WAIT_TIMEOUT_MS / 1000}s`,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
161
262
|
|
|
162
263
|
// Wait for the runner BEFORE invoking, not after. An event sent early is
|
|
163
264
|
// dropped rather than queued, so there is nothing to wait for afterwards.
|
|
@@ -188,7 +289,7 @@ export const automationRun: Command = {
|
|
|
188
289
|
const before = await client.runs(slug, 1)
|
|
189
290
|
const priorId = before[0]?.id ?? null
|
|
190
291
|
|
|
191
|
-
const queued = await client.run(slug)
|
|
292
|
+
const queued = await client.run(slug, version)
|
|
192
293
|
|
|
193
294
|
if (flagBool(ctx, 'no-wait')) {
|
|
194
295
|
return {
|
|
@@ -202,6 +303,8 @@ export const automationRun: Command = {
|
|
|
202
303
|
let seen: AutomationRunSummary | undefined
|
|
203
304
|
while (Date.now() < deadline) {
|
|
204
305
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS))
|
|
306
|
+
// The LIVE path. No mode to pass — `runs()` defaults to live, which is
|
|
307
|
+
// what this loop is waiting for.
|
|
205
308
|
const [latest] = await client.runs(slug, 1)
|
|
206
309
|
if (isNewRun(priorId, latest) && latest) {
|
|
207
310
|
seen = latest
|
|
@@ -237,20 +340,34 @@ export const automationRuns: Command = {
|
|
|
237
340
|
noun: 'automation',
|
|
238
341
|
verb: 'runs',
|
|
239
342
|
args: [{ name: 'slug', required: true, description: 'automation slug' }],
|
|
240
|
-
flags: {},
|
|
343
|
+
flags: { dev: 'boolean' },
|
|
241
344
|
summary: 'List recent runs, newest first, with what each one returned',
|
|
242
|
-
examples: [
|
|
345
|
+
examples: [
|
|
346
|
+
'frontera automation runs daily-digest',
|
|
347
|
+
'frontera automation runs daily-digest --dev',
|
|
348
|
+
],
|
|
243
349
|
},
|
|
244
350
|
|
|
245
351
|
async run(ctx) {
|
|
246
352
|
const slug = requireSlug(ctx)
|
|
247
|
-
|
|
353
|
+
// The two histories are separate rows and the route defaults to live, so
|
|
354
|
+
// without this a dev run is invisible from the CLI — the same blind spot
|
|
355
|
+
// that made `run --dev` wait out its timeout.
|
|
356
|
+
const dev = flagBool(ctx, 'dev')
|
|
357
|
+
const rows = await new AutomationApi(ctx.apiUrl, ctx.token).runs(
|
|
358
|
+
slug,
|
|
359
|
+
undefined,
|
|
360
|
+
dev ? 'dev' : 'live',
|
|
361
|
+
)
|
|
248
362
|
|
|
249
363
|
if (rows.length === 0) {
|
|
250
364
|
return {
|
|
251
365
|
data: rows,
|
|
252
|
-
text:
|
|
253
|
-
|
|
366
|
+
text: dev
|
|
367
|
+
? `No dev runs recorded for ${slug}.\n`
|
|
368
|
+
+ ` frontera automation dev <entry-file> # to serve one`
|
|
369
|
+
: `No runs recorded for ${slug}.\n`
|
|
370
|
+
+ ` frontera automation run ${slug} # to start one`,
|
|
254
371
|
}
|
|
255
372
|
}
|
|
256
373
|
|