@junoflow/cli 0.13.0 → 0.15.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.
package/README.md CHANGED
@@ -165,9 +165,18 @@ The group's working-copy commands (`integration init`, `gen-types`,
165
165
 
166
166
  **`finding`**
167
167
 
168
- | Command | Tool | Positional | Effect |
169
- | ------------------ | ------------- | ---------- | ------ |
170
- | `juno finding get` | `get_finding` | `slug` | read |
168
+ | Command | Tool | Positional | Effect |
169
+ | -------------------------- | --------------------- | ---------- | ------ |
170
+ | `juno finding list` | `list_findings` | — | read |
171
+ | `juno finding get` | `get_finding` | `slug` | read |
172
+ | `juno finding get-history` | `get_finding_history` | `slug` | read |
173
+ | `juno finding dismiss` | `dismiss_finding` | `slug` | act |
174
+
175
+ `finding list` shows open findings of every owner — flows, integrations,
176
+ devices, states and tasks — each naming its source as `owner_kind`, `owner` and
177
+ `owner_name`; `--status all` adds cleared ones, and `--owner-kind`, `--owner`
178
+ and `--severity` (a floor) narrow it. `get-history` and `dismiss` take `--key`
179
+ for a keyed finding; `get-history` also takes `--limit`.
171
180
 
172
181
  **`webhook`**
173
182
 
@@ -178,14 +187,22 @@ The group's working-copy commands (`integration init`, `gen-types`,
178
187
  | `juno webhook write` | `write_webhook` | `id` | change |
179
188
  | `juno webhook fire` | `fire_webhook` | `id` | act |
180
189
 
181
- **`job`** (external jobs)
190
+ **`task`** (tasks)
182
191
 
183
- | Command | Tool | Positional | Effect |
184
- | ------------------------------------ | -------------- | ---------- | ------ |
185
- | `juno job list` | `list_jobs` | — | read |
186
- | `juno job get-runs` | `get_job_runs` | `id` | read |
187
- | `juno job write` | `write_job` | `id` | change |
188
- | `juno job run [<url>] -- <command>…` | none | ping URL | act |
192
+ | Command | Tool | Positional | Effect |
193
+ | ------------------------------------- | --------------- | ---------- | ------ |
194
+ | `juno task list` | `list_tasks` | — | read |
195
+ | `juno task get-runs` | `get_task_runs` | `id` | read |
196
+ | `juno task write` | `write_task` | `id` | change |
197
+ | `juno task delete` | `delete_task` | `id` | act |
198
+ | `juno task run [<url>] -- <command>…` | none | ping URL | act |
199
+ | `juno task ping [<url>]` | none | ping URL | act |
200
+
201
+ **`series`**
202
+
203
+ | Command | Tool | Positional | Effect |
204
+ | ----------------- | ------------ | ---------- | ------ |
205
+ | `juno series get` | `get_series` | `id` | read |
189
206
 
190
207
  **`credential`**
191
208
 
@@ -336,15 +353,15 @@ exits 1 without sending anything otherwise. Any other message sent to a session
336
353
  waiting for an approval denies it, and the assistant sees the message as the
337
354
  reason.
338
355
 
339
- ### `juno job run`
356
+ ### `juno task run`
340
357
 
341
- Wraps a scheduled command so its runs are reported to an external job's ping URL
342
- (`juno job write` answers it as `url`):
358
+ Wraps a scheduled command so its runs are reported to a task's ping URL
359
+ (`juno task write` answers it as `url`):
343
360
 
344
361
  ```bash
345
- juno job run https://juno.example/ping/<token> -- ./backup.sh --full
346
- juno job run --url-file .secrets/juno-jobs/backup -- ./backup.sh --full
347
- JUNO_JOB_URL=https://juno.example/ping/<token> juno job run -- python3 check.py
362
+ juno task run https://juno.example/ping/<token> -- ./backup.sh --full
363
+ juno task run --url-file .secrets/juno-tasks/backup -- ./backup.sh --full
364
+ JUNO_TASK_URL=https://juno.example/ping/<token> juno task run -- python3 check.py
348
365
  ```
349
366
 
350
367
  The command runs exactly as it would unwrapped: same stdin, the same stdout and
@@ -353,14 +370,33 @@ signal `N`; 127, or 126 for a file that is not executable, when it cannot be
353
370
  started). Everything after `--` is the command's, flags included. Juno gets a
354
371
  `start`, then the exit status with the last 64 KiB of stdout and stderr, paired
355
372
  by a run id. The URL comes from the argument, else `--url-file` (keeps the token
356
- out of `ps`), else `$JUNO_JOB_URL`; nothing else is read — no instance config,
373
+ out of `ps`), else `$JUNO_TASK_URL`; nothing else is read — no instance config,
357
374
  no API key. A report that fails is retried once and then dropped silently
358
375
  (`--verbose` prints one line): Juno turns a run it never heard about into an
359
376
  overdue finding by itself. `SIGINT`, `SIGTERM` and `SIGHUP` are passed on to the
360
377
  command.
361
378
 
362
- See [External jobs](https://boivie.github.io/juno/external-jobs/) for creating
363
- the job, the ping protocol, and systemd and crontab examples.
379
+ The wrapper never times a command out: a run that hangs stays open, and the task
380
+ goes down once its start plus grace has passed. A wrapper killed outright leaves
381
+ the run open the same way.
382
+
383
+ ### `juno task ping`
384
+
385
+ Sends one report, for a task with no command to wrap:
386
+
387
+ ```bash
388
+ juno task ping --url-file .secrets/juno-tasks/digest # success
389
+ juno task ping --url-file .secrets/juno-tasks/digest --start --rid r1
390
+ juno task ping --url-file .secrets/juno-tasks/digest --exit 3 --rid r1
391
+ juno task ping --url-file .secrets/juno-tasks/digest --fail
392
+ ```
393
+
394
+ The URL comes from the same three places as `juno task run`'s. `--log` sends a
395
+ log report, and `--rid` pairs a start with its finish. A report that still fails
396
+ after one retry exits 1 with the reason.
397
+
398
+ See [Tasks](https://boivie.github.io/juno/tasks/) for creating the task, the
399
+ ping protocol, and systemd and crontab examples.
364
400
 
365
401
  ### `juno credential set`
366
402
 
package/app-client.js CHANGED
@@ -14,27 +14,36 @@
14
14
  * rejects with an Error whose message is the handler's (or the host's).
15
15
  *
16
16
  * window.juno.values
17
- * The app's bound states and controls, by binding name:
18
- * {states: {<b>: value}, controls: {<b>: {value, active, expires_at}}}, or
19
- * null until the first snapshot has arrived.
17
+ * The app's bound states, controls and series, by binding name:
18
+ * {states: {<b>: value}, controls: {<b>: {value, active, expires_at}},
19
+ * series: {<b>: {published_at, issued_at, covered_until, intervals}}}, or
20
+ * null until the first snapshot has arrived. A series moves on every
21
+ * publish, changed or not.
20
22
  *
21
23
  * window.juno.watch(cb) → unwatch()
22
24
  * Calls cb(values, changed) on every snapshot and update, where changed
23
- * lists the keys that moved ("states.<b>", "controls.<b>"; every key on a
24
- * snapshot). Registered after a snapshot, cb is called at once with every
25
- * key.
25
+ * lists the keys that moved ("states.<b>", "controls.<b>", "series.<b>";
26
+ * every key on a snapshot). Registered after a snapshot, cb is called at
27
+ * once with every key.
28
+ *
29
+ * window.juno.series.<binding>.get(range?) → Promise<read>
30
+ * window.juno.series.<binding>.view(spec) → Promise<result>
31
+ * What a handler's series.<binding>.get / .view answer, with no handler:
32
+ * GET _juno/series/<binding>?from=&to= and POST _juno/series/<binding>/view.
33
+ * A Date bound is sent as its ISO string.
26
34
  *
27
35
  * window.juno.controls.<binding>.onPress(cb) → unwatch()
28
36
  * Calls cb(at) on every press of that bound button control, at an RFC 3339
29
37
  * timestamp.
30
38
  *
31
- * handlers and controls are proxies, not maps of what the manifest declares:
39
+ * handlers, controls and series are proxies, not maps of what the manifest declares:
32
40
  * the client is never told the names, so any name reaches the server, and an
33
41
  * unknown handler is a rejected call carrying the host's 404 — the same answer
34
42
  * a hand-typed name got before. A name some JavaScript protocol looks up on
35
43
  * every object (then, toJSON, toString, …) answers undefined instead, so
36
44
  * `await juno.handlers` or JSON.stringify(juno) never fires a call;
37
- * manifest.ValidateApp refuses those names for a handler or a control binding.
45
+ * manifest.ValidateApp refuses those names for a handler, a control binding or
46
+ * a series binding.
38
47
  *
39
48
  * The values arrive over one EventSource on _juno/stream, opened by the first
40
49
  * watch or onPress and never on page load: a page that does not watch holds no
@@ -147,6 +156,67 @@
147
156
  return body.result;
148
157
  }
149
158
 
159
+ // readJSON answers a series route: the body on success, its error thrown
160
+ // otherwise. Like a call, every answer names the version that served it.
161
+ async function readJSON(res) {
162
+ checkVersion(res);
163
+ var body;
164
+ try {
165
+ body = await res.json();
166
+ } catch (e) {
167
+ throw new Error('HTTP ' + res.status);
168
+ }
169
+ if (!res.ok) {
170
+ throw new Error(
171
+ body && typeof body.error === 'string'
172
+ ? body.error
173
+ : 'HTTP ' + res.status
174
+ );
175
+ }
176
+ return body;
177
+ }
178
+
179
+ function instant(v) {
180
+ // Duck-typed rather than instanceof, so a Date from another realm (an
181
+ // iframe) is still sent as its ISO string.
182
+ return v && typeof v.toISOString === 'function'
183
+ ? v.toISOString()
184
+ : String(v);
185
+ }
186
+
187
+ function seriesURL(binding) {
188
+ return base + '_juno/series/' + encodeURIComponent(binding);
189
+ }
190
+
191
+ async function seriesGet(binding, range) {
192
+ var q = [];
193
+ if (range && range.from !== undefined && range.from !== null) {
194
+ q.push('from=' + encodeURIComponent(instant(range.from)));
195
+ }
196
+ if (range && range.to !== undefined && range.to !== null) {
197
+ q.push('to=' + encodeURIComponent(instant(range.to)));
198
+ }
199
+ var res = await fetch(
200
+ seriesURL(binding) + (q.length ? '?' + q.join('&') : ''),
201
+ {
202
+ headers: { 'X-Juno-App-Version': String(version) },
203
+ }
204
+ );
205
+ return readJSON(res);
206
+ }
207
+
208
+ async function seriesView(binding, spec) {
209
+ var res = await fetch(seriesURL(binding) + '/view', {
210
+ method: 'POST',
211
+ headers: {
212
+ 'Content-Type': 'application/json',
213
+ 'X-Juno-App-Version': String(version),
214
+ },
215
+ body: JSON.stringify(spec === undefined ? {} : spec),
216
+ });
217
+ return readJSON(res);
218
+ }
219
+
150
220
  // Live values. The stream is opened lazily and at most one is open.
151
221
  var values = null;
152
222
  var watchers = [];
@@ -190,6 +260,9 @@
190
260
  Object.keys(vals.controls || {}).forEach(function (b) {
191
261
  out.push('controls.' + b);
192
262
  });
263
+ Object.keys(vals.series || {}).forEach(function (b) {
264
+ out.push('series.' + b);
265
+ });
193
266
  return out;
194
267
  }
195
268
 
@@ -207,6 +280,7 @@
207
280
  values = {
208
281
  states: data.values.states || {},
209
282
  controls: data.values.controls || {},
283
+ series: data.values.series || {},
210
284
  };
211
285
  var changed = keys(values);
212
286
  watchers.slice().forEach(function (cb) {
@@ -214,7 +288,7 @@
214
288
  });
215
289
  }
216
290
 
217
- // onUpdate merges per binding. An update carries both maps, the one that did
291
+ // onUpdate merges per binding. An update carries every map, the ones that did
218
292
  // not move empty, so the changed keys are exactly the bindings present.
219
293
  function onUpdate(ev) {
220
294
  var data = parse(ev);
@@ -222,10 +296,12 @@
222
296
  var moved = {
223
297
  states: data.values.states || {},
224
298
  controls: data.values.controls || {},
299
+ series: data.values.series || {},
225
300
  };
226
301
  values = {
227
302
  states: Object.assign({}, values.states, moved.states),
228
303
  controls: Object.assign({}, values.controls, moved.controls),
304
+ series: Object.assign({}, values.series, moved.series),
229
305
  };
230
306
  var changed = keys(moved);
231
307
  if (!changed.length) return;
@@ -298,6 +374,16 @@
298
374
  },
299
375
  });
300
376
  }),
377
+ series: namespace(function (binding) {
378
+ return Object.freeze({
379
+ get: function (range) {
380
+ return seriesGet(binding, range);
381
+ },
382
+ view: function (spec) {
383
+ return seriesView(binding, spec);
384
+ },
385
+ });
386
+ }),
301
387
  get values() {
302
388
  return values;
303
389
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junoflow/cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Develop Juno flows, integrations and apps, and operate a Juno instance from the command line",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,7 +40,7 @@
40
40
  "node": ">=20"
41
41
  },
42
42
  "dependencies": {
43
- "@junoflow/eslint-plugin": "0.13.0",
43
+ "@junoflow/eslint-plugin": "0.15.0",
44
44
  "@typescript-eslint/parser": "^8.0.0"
45
45
  },
46
46
  "devDependencies": {
@@ -6,9 +6,10 @@ import { resolve } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
 
8
8
  import { UsageError } from '../errors.js';
9
+ import { EXIT_FAILURE, EXIT_OK } from '../exit.js';
9
10
 
10
- export const JOB_RUN_USAGE =
11
- 'juno job run [<url>] [--url-file <path>] [--verbose] -- <command> [args…]';
11
+ export const TASK_RUN_USAGE =
12
+ 'juno task run [<url>] [--url-file <path>] [--verbose] -- <command> [args…]';
12
13
 
13
14
  /** The tail of each stream a run reports; Juno keeps the same. */
14
15
  export const TAIL_BYTES = 64 << 10;
@@ -23,7 +24,7 @@ const DEFAULT_TIMEOUTS = {
23
24
  };
24
25
 
25
26
  /**
26
- * `juno job run` runs a command and reports it to an external job's ping URL
27
+ * `juno task run` runs a command and reports it to a task's ping URL
27
28
  * (plan-external-jobs-2-wrapper-and-tools.md, Decisions 3–5). Its first rule
28
29
  * is transparency: the command gets the same stdin, and the wrapper's stdout,
29
30
  * stderr and exit status are the command's, byte for byte — a scheduler such
@@ -33,11 +34,11 @@ const DEFAULT_TIMEOUTS = {
33
34
  * on its own, so the wrapper never queues and never fails the command.
34
35
  *
35
36
  * It reads no Juno config and no API key; the URL is all it needs. argv is
36
- * everything after `job run`, `--` included. The exit code is returned, not
37
+ * everything after `task run`, `--` included. The exit code is returned, not
37
38
  * passed to process.exit, so stdout is flushed before the process ends.
38
39
  * deps is injectable for the tests; the default binds the real ones.
39
40
  */
40
- export async function jobRun(argv, io, deps = {}) {
41
+ export async function taskRun(argv, io, deps = {}) {
41
42
  const {
42
43
  spawn = nodeSpawn,
43
44
  fetch = globalThis.fetch,
@@ -64,20 +65,26 @@ export async function jobRun(argv, io, deps = {}) {
64
65
  });
65
66
  } catch (err) {
66
67
  throw new UsageError(
67
- `juno job run: ${err.message}\nusage: ${JOB_RUN_USAGE}`
68
+ `juno task run: ${err.message}\nusage: ${TASK_RUN_USAGE}`
68
69
  );
69
70
  }
70
71
  if (command.length === 0) {
71
72
  throw new UsageError(
72
- `juno job run: missing -- <command>\nusage: ${JOB_RUN_USAGE}`
73
+ `juno task run: missing -- <command>\nusage: ${TASK_RUN_USAGE}`
73
74
  );
74
75
  }
75
76
  if (parsed.positionals.length > 1) {
76
77
  throw new UsageError(
77
- `juno job run: one URL before --, got ${parsed.positionals.length} arguments\nusage: ${JOB_RUN_USAGE}`
78
+ `juno task run: one URL before --, got ${parsed.positionals.length} arguments\nusage: ${TASK_RUN_USAGE}`
78
79
  );
79
80
  }
80
- const url = await pingURL(parsed, env, readFile, io.cwd);
81
+ const url = await pingURL('task run', TASK_RUN_USAGE, {
82
+ positional: parsed.positionals[0],
83
+ file: parsed.values['url-file'],
84
+ env,
85
+ readFile,
86
+ cwd: io.cwd,
87
+ });
81
88
  const verbose = parsed.values.verbose;
82
89
 
83
90
  const rid = randomUUID();
@@ -100,7 +107,11 @@ export async function jobRun(argv, io, deps = {}) {
100
107
  let suffix;
101
108
  let exitCode;
102
109
  if (outcome.startError) {
103
- const line = `juno job run: ${command[0]}: ${outcome.startError.message}`;
110
+ // With no shell, `FOO=bar cmd` names an executable called "FOO=bar".
111
+ const line =
112
+ outcome.startError.code === 'ENOENT' && command[0].includes('=')
113
+ ? `juno task run: "${command[0]}" looks like an env assignment; write -- env ${command.join(' ')}`
114
+ : `juno task run: ${command[0]}: ${outcome.startError.message}`;
104
115
  io.error(line);
105
116
  err.push(Buffer.from(`${line}\n`));
106
117
  suffix = 'fail';
@@ -129,46 +140,55 @@ export async function jobRun(argv, io, deps = {}) {
129
140
  failure = await finish();
130
141
  }
131
142
  if (failure && verbose) {
132
- io.error(`juno job run: could not report to Juno: ${failure}`);
143
+ io.error(`juno task run: could not report to Juno: ${failure}`);
133
144
  }
134
145
  return exitCode;
135
146
  }
136
147
 
137
148
  /**
138
149
  * pingURL is the URL from the first source that yields one: the positional,
139
- * then --url-file (its contents, trimmed), then $JUNO_JOB_URL. The two
150
+ * then --url-file (its contents, trimmed), then $JUNO_TASK_URL. The two
140
151
  * indirect sources keep the token off the command line, where `ps` and the
141
- * scheduler's job definition would show it.
152
+ * scheduler's job definition would show it. command and usage name the verb
153
+ * in its refusals.
142
154
  */
143
- async function pingURL(parsed, env, readFile, cwd = '.') {
144
- const [positional] = parsed.positionals;
155
+ async function pingURL(
156
+ command,
157
+ usage,
158
+ { positional, file, env, readFile, cwd = '.' }
159
+ ) {
145
160
  if (positional?.trim()) return positional.trim();
146
- const file = parsed.values['url-file'];
147
161
  if (file !== undefined) {
148
162
  let text;
149
163
  try {
150
164
  text = await readFile(resolve(cwd, file), 'utf8');
151
165
  } catch (err) {
152
- throw new UsageError(`juno job run: --url-file ${file}: ${err.message}`);
166
+ throw new UsageError(
167
+ `juno ${command}: --url-file ${file}: ${err.message}`
168
+ );
153
169
  }
154
170
  if (text.trim()) return text.trim();
155
171
  }
156
- if (env.JUNO_JOB_URL?.trim()) return env.JUNO_JOB_URL.trim();
172
+ if (env.JUNO_TASK_URL?.trim()) return env.JUNO_TASK_URL.trim();
157
173
  throw new UsageError(
158
- `juno job run: no ping URL — pass it, --url-file or JUNO_JOB_URL\nusage: ${JOB_RUN_USAGE}`
174
+ `juno ${command}: no ping URL — pass it, --url-file or JUNO_TASK_URL\nusage: ${usage}`
159
175
  );
160
176
  }
161
177
 
162
178
  /**
163
179
  * endpoint is `<url>/<suffix>?rid=<rid>`, used as given otherwise: no /ping/
164
- * check, so a Healthchecks URL works too. A query the URL already carries is
165
- * kept.
180
+ * check, so a Healthchecks URL works too. An empty suffix is a success, which
181
+ * is the bare URL. A query the URL already carries is kept; without a rid
182
+ * there is no `rid` parameter.
166
183
  */
167
184
  export function endpoint(url, suffix, rid) {
168
185
  const q = url.indexOf('?');
169
- const path = (q === -1 ? url : url.slice(0, q)).replace(/\/+$/, '');
170
- const query = q === -1 ? '' : `${url.slice(q + 1)}&`;
171
- return `${path}/${suffix}?${query}rid=${encodeURIComponent(rid)}`;
186
+ const base = (q === -1 ? url : url.slice(0, q)).replace(/\/+$/, '');
187
+ const path = suffix ? `${base}/${suffix}` : base;
188
+ const params = [];
189
+ if (q !== -1 && q < url.length - 1) params.push(url.slice(q + 1));
190
+ if (rid) params.push(`rid=${encodeURIComponent(rid)}`);
191
+ return params.length ? `${path}?${params.join('&')}` : path;
172
192
  }
173
193
 
174
194
  /**
@@ -274,3 +294,75 @@ export class Tail {
274
294
  return Buffer.concat(this.#chunks, this.#size);
275
295
  }
276
296
  }
297
+
298
+ export const TASK_PING_USAGE =
299
+ 'juno task ping [<url>] [--url-file <path>] [--start | --fail | --exit <0-255> | --log] [--rid <id>]';
300
+
301
+ const PING_KINDS = ['start', 'fail', 'exit', 'log'];
302
+
303
+ /**
304
+ * `juno task ping` sends one report to a task's ping URL, for a task
305
+ * `task run` cannot wrap — one whose scheduler runs no command, or whose start
306
+ * and finish happen in different places. Without a flag it reports success;
307
+ * `--start`, `--fail`, `--exit <n>` and `--log` send the other reports, and
308
+ * `--rid` pairs a start with its finish. It sends no output.
309
+ *
310
+ * It finds the URL as `task run` does and, like it, reads no Juno config and no
311
+ * API key. Unlike `task run` it has nothing else to do, so a report that still
312
+ * fails after one retry is exit 1 with the reason, not silence: the caller
313
+ * asked for exactly this and can `|| true` it.
314
+ */
315
+ export async function taskPing(args, io, deps = {}) {
316
+ const {
317
+ fetch = globalThis.fetch,
318
+ env = process.env,
319
+ readFile = nodeReadFile,
320
+ timeouts = DEFAULT_TIMEOUTS,
321
+ } = deps;
322
+ const usage = (msg) =>
323
+ new UsageError(`juno task ping: ${msg}\nusage: ${TASK_PING_USAGE}`);
324
+
325
+ if (args._.length > 1) {
326
+ throw usage(`one URL, got ${args._.length} arguments`);
327
+ }
328
+ const kinds = PING_KINDS.filter(
329
+ (k) => args[k] !== undefined && args[k] !== false
330
+ );
331
+ if (kinds.length > 1) {
332
+ throw usage(`--${kinds.join(' and --')} are different reports; pick one`);
333
+ }
334
+ // A success is the bare URL: the protocol has no /success suffix.
335
+ let suffix = '';
336
+ if (kinds[0] === 'exit') {
337
+ const n = args.exit.trim();
338
+ if (!/^\d{1,3}$/.test(n) || Number(n) > 255) {
339
+ throw usage(`--exit takes a status from 0 to 255, got "${args.exit}"`);
340
+ }
341
+ suffix = String(Number(n));
342
+ } else if (kinds[0]) {
343
+ suffix = kinds[0];
344
+ }
345
+ const url = await pingURL('task ping', TASK_PING_USAGE, {
346
+ positional: args._[0],
347
+ file: args['url-file'],
348
+ env,
349
+ readFile,
350
+ cwd: io.cwd,
351
+ });
352
+
353
+ const send = () =>
354
+ report(fetch, endpoint(url, suffix, args.rid), {
355
+ method: 'GET',
356
+ timeout: timeouts.finish,
357
+ });
358
+ let failure = await send();
359
+ if (failure) {
360
+ await new Promise((resolve) => setTimeout(resolve, timeouts.retryDelay));
361
+ failure = await send();
362
+ }
363
+ if (failure) {
364
+ io.error(`juno task ping: could not report to Juno: ${failure}`);
365
+ return EXIT_FAILURE;
366
+ }
367
+ return EXIT_OK;
368
+ }
package/src/exit.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * 5 `juno ask` / `session approve`: the turn is still running (pending)
14
14
  * 6 `juno ask` / `session approve`: the assistant is awaiting input
15
15
  *
16
- * `juno job run` is the exception once its own arguments are accepted: it
16
+ * `juno task run` is the exception once its own arguments are accepted: it
17
17
  * exits with the wrapped command's status, as a shell would report it.
18
18
  */
19
19
  export const EXIT_OK = 0;
package/src/index.js CHANGED
@@ -29,7 +29,12 @@ import { FLOW_INIT_USAGE, flowInit } from './commands/flow-init.js';
29
29
  import { FLOW_PULL_USAGE, flowPull } from './commands/flow-pull.js';
30
30
  import { GEN_TYPES_USAGE, genTypes } from './commands/gen-types.js';
31
31
  import { INIT_USAGE, init } from './commands/init.js';
32
- import { JOB_RUN_USAGE, jobRun } from './commands/job-run.js';
32
+ import {
33
+ TASK_PING_USAGE,
34
+ TASK_RUN_USAGE,
35
+ taskPing,
36
+ taskRun,
37
+ } from './commands/task-run.js';
33
38
  import { PULL_USAGE, pull } from './commands/pull.js';
34
39
  import { SKILL_USAGE, skill } from './commands/skill.js';
35
40
  import {
@@ -106,15 +111,29 @@ const NON_TOOL = {
106
111
  // raw: everything after `--` is the wrapped command's, verbatim, so the
107
112
  // argv goes to the handler unparsed (see runRow). No --url or --token: the
108
113
  // wrapper reads no Juno config, only the ping URL.
109
- 'job run': {
110
- run: (argv, io) => jobRun(argv, io, { env: io.env }),
114
+ 'task run': {
115
+ run: (argv, io) => taskRun(argv, io, { env: io.env }),
111
116
  options: {
112
117
  'url-file': { type: 'string' },
113
118
  verbose: { type: 'boolean', default: false },
114
119
  },
115
- usage: JOB_RUN_USAGE,
120
+ usage: TASK_RUN_USAGE,
116
121
  raw: true,
117
122
  },
123
+ // No --url or --token either: one report to the ping URL, found as task run
124
+ // finds it.
125
+ 'task ping': {
126
+ run: (args, io) => taskPing(args, io, { env: io.env }),
127
+ options: {
128
+ 'url-file': { type: 'string' },
129
+ start: { type: 'boolean' },
130
+ fail: { type: 'boolean' },
131
+ exit: { type: 'string' },
132
+ log: { type: 'boolean' },
133
+ rid: { type: 'string' },
134
+ },
135
+ usage: TASK_PING_USAGE,
136
+ },
118
137
  'config set': { run: configSet, options: {}, usage: CONFIG_SET_USAGE },
119
138
  'agent-skill install': {
120
139
  run: agentSkillInstall,
package/verbs.json CHANGED
@@ -212,7 +212,32 @@
212
212
  "working_copy": true,
213
213
  "summary": "Serve this app locally on mocked data"
214
214
  },
215
+ {
216
+ "command": "finding list",
217
+ "tool": "list_findings",
218
+ "flags": {
219
+ "status": { "field": "status", "type": "string" },
220
+ "owner-kind": { "field": "owner_kind", "type": "string" },
221
+ "owner": { "field": "owner", "type": "string" },
222
+ "severity": { "field": "severity", "type": "string" }
223
+ }
224
+ },
215
225
  { "command": "finding get", "tool": "get_finding", "id_field": "slug" },
226
+ {
227
+ "command": "finding get-history",
228
+ "tool": "get_finding_history",
229
+ "id_field": "slug",
230
+ "flags": {
231
+ "key": { "field": "key", "type": "string" },
232
+ "limit": { "field": "limit", "type": "integer" }
233
+ }
234
+ },
235
+ {
236
+ "command": "finding dismiss",
237
+ "tool": "dismiss_finding",
238
+ "id_field": "slug",
239
+ "flags": { "key": { "field": "key", "type": "string" } }
240
+ },
216
241
  { "command": "webhook list", "tool": "list_webhooks" },
217
242
  {
218
243
  "command": "webhook get-deliveries",
@@ -221,9 +246,12 @@
221
246
  },
222
247
  { "command": "webhook write", "tool": "write_webhook", "id_field": "id" },
223
248
  { "command": "webhook fire", "tool": "fire_webhook", "id_field": "id" },
224
- { "command": "job list", "tool": "list_jobs" },
225
- { "command": "job get-runs", "tool": "get_job_runs", "id_field": "id" },
226
- { "command": "job write", "tool": "write_job", "id_field": "id" },
249
+ { "command": "task list", "tool": "list_tasks" },
250
+ { "command": "task get-runs", "tool": "get_task_runs", "id_field": "id" },
251
+ { "command": "task write", "tool": "write_task", "id_field": "id" },
252
+ { "command": "task delete", "tool": "delete_task", "id_field": "id" },
253
+ { "command": "task command", "tool": "command_task", "id_field": "id" },
254
+ { "command": "series get", "tool": "get_series", "id_field": "id" },
227
255
  { "command": "credential list", "tool": "list_credentials" },
228
256
  { "command": "person list", "tool": "list_people" },
229
257
  { "command": "person get", "tool": "get_person", "id_field": "id" },
@@ -239,6 +267,16 @@
239
267
  "id_field": "run_id",
240
268
  "flags": { "handle": { "field": "handle_id", "type": "string" } }
241
269
  },
270
+ {
271
+ "command": "question answer",
272
+ "tool": "answer_question",
273
+ "id_field": "run_id",
274
+ "flags": {
275
+ "handle": { "field": "handle_id", "type": "string" },
276
+ "answer": { "field": "answer", "type": "string" },
277
+ "for": { "field": "for", "type": "string" }
278
+ }
279
+ },
242
280
  {
243
281
  "command": "ha get-entity",
244
282
  "tool": "get_entity",
@@ -275,9 +313,14 @@
275
313
  "summary": "Approve the tool call an assistant session is waiting on"
276
314
  },
277
315
  {
278
- "command": "job run",
316
+ "command": "task run",
317
+ "effect": "act",
318
+ "summary": "Run a command and report it to a task's ping URL"
319
+ },
320
+ {
321
+ "command": "task ping",
279
322
  "effect": "act",
280
- "summary": "Run a command and report it to an external job's ping URL"
323
+ "summary": "Send one report (success, start, fail, an exit status or a log) to a task's ping URL"
281
324
  },
282
325
  {
283
326
  "command": "credential set",