@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.
@@ -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
- const lines = [`${run.status} v${run.version}, ${run.ctxCalls} ctx call(s)`]
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: ['frontera automation runs daily-digest'],
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
- const rows = await new AutomationApi(ctx.apiUrl, ctx.token).runs(slug)
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: `No runs recorded for ${slug}.\n`
253
- + ` frontera automation run ${slug} # to start one`,
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