@junoflow/cli 0.14.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 +43 -28
- package/app-client.js +95 -9
- package/package.json +2 -2
- package/src/commands/{job-run.js → task-run.js} +36 -31
- package/src/exit.js +1 -1
- package/src/index.js +12 -12
- package/verbs.json +45 -8
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
|
|
169
|
-
|
|
|
170
|
-
| `juno finding
|
|
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,16 +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
|
-
**`
|
|
190
|
+
**`task`** (tasks)
|
|
182
191
|
|
|
183
|
-
| Command
|
|
184
|
-
|
|
|
185
|
-
| `juno
|
|
186
|
-
| `juno
|
|
187
|
-
| `juno
|
|
188
|
-
| `juno
|
|
189
|
-
| `juno
|
|
190
|
-
| `juno
|
|
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 |
|
|
191
206
|
|
|
192
207
|
**`credential`**
|
|
193
208
|
|
|
@@ -338,15 +353,15 @@ exits 1 without sending anything otherwise. Any other message sent to a session
|
|
|
338
353
|
waiting for an approval denies it, and the assistant sees the message as the
|
|
339
354
|
reason.
|
|
340
355
|
|
|
341
|
-
### `juno
|
|
356
|
+
### `juno task run`
|
|
342
357
|
|
|
343
|
-
Wraps a scheduled command so its runs are reported to
|
|
344
|
-
(`juno
|
|
358
|
+
Wraps a scheduled command so its runs are reported to a task's ping URL
|
|
359
|
+
(`juno task write` answers it as `url`):
|
|
345
360
|
|
|
346
361
|
```bash
|
|
347
|
-
juno
|
|
348
|
-
juno
|
|
349
|
-
|
|
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
|
|
350
365
|
```
|
|
351
366
|
|
|
352
367
|
The command runs exactly as it would unwrapped: same stdin, the same stdout and
|
|
@@ -355,33 +370,33 @@ signal `N`; 127, or 126 for a file that is not executable, when it cannot be
|
|
|
355
370
|
started). Everything after `--` is the command's, flags included. Juno gets a
|
|
356
371
|
`start`, then the exit status with the last 64 KiB of stdout and stderr, paired
|
|
357
372
|
by a run id. The URL comes from the argument, else `--url-file` (keeps the token
|
|
358
|
-
out of `ps`), else `$
|
|
373
|
+
out of `ps`), else `$JUNO_TASK_URL`; nothing else is read — no instance config,
|
|
359
374
|
no API key. A report that fails is retried once and then dropped silently
|
|
360
375
|
(`--verbose` prints one line): Juno turns a run it never heard about into an
|
|
361
376
|
overdue finding by itself. `SIGINT`, `SIGTERM` and `SIGHUP` are passed on to the
|
|
362
377
|
command.
|
|
363
378
|
|
|
364
|
-
The wrapper never times a command out: a run that hangs stays open, and the
|
|
379
|
+
The wrapper never times a command out: a run that hangs stays open, and the task
|
|
365
380
|
goes down once its start plus grace has passed. A wrapper killed outright leaves
|
|
366
381
|
the run open the same way.
|
|
367
382
|
|
|
368
|
-
### `juno
|
|
383
|
+
### `juno task ping`
|
|
369
384
|
|
|
370
385
|
Sends one report, for a task with no command to wrap:
|
|
371
386
|
|
|
372
387
|
```bash
|
|
373
|
-
juno
|
|
374
|
-
juno
|
|
375
|
-
juno
|
|
376
|
-
juno
|
|
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
|
|
377
392
|
```
|
|
378
393
|
|
|
379
|
-
The URL comes from the same three places as `juno
|
|
394
|
+
The URL comes from the same three places as `juno task run`'s. `--log` sends a
|
|
380
395
|
log report, and `--rid` pairs a start with its finish. A report that still fails
|
|
381
396
|
after one retry exits 1 with the reason.
|
|
382
397
|
|
|
383
|
-
See [
|
|
384
|
-
|
|
398
|
+
See [Tasks](https://boivie.github.io/juno/tasks/) for creating the task, the
|
|
399
|
+
ping protocol, and systemd and crontab examples.
|
|
385
400
|
|
|
386
401
|
### `juno credential set`
|
|
387
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
|
|
18
|
-
* {states: {<b>: value}, controls: {<b>: {value, active, expires_at}}
|
|
19
|
-
*
|
|
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>";
|
|
24
|
-
* snapshot). Registered after a snapshot, cb is called at
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
43
|
+
"@junoflow/eslint-plugin": "0.15.0",
|
|
44
44
|
"@typescript-eslint/parser": "^8.0.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -8,8 +8,8 @@ import { parseArgs } from 'node:util';
|
|
|
8
8
|
import { UsageError } from '../errors.js';
|
|
9
9
|
import { EXIT_FAILURE, EXIT_OK } from '../exit.js';
|
|
10
10
|
|
|
11
|
-
export const
|
|
12
|
-
'juno
|
|
11
|
+
export const TASK_RUN_USAGE =
|
|
12
|
+
'juno task run [<url>] [--url-file <path>] [--verbose] -- <command> [args…]';
|
|
13
13
|
|
|
14
14
|
/** The tail of each stream a run reports; Juno keeps the same. */
|
|
15
15
|
export const TAIL_BYTES = 64 << 10;
|
|
@@ -24,7 +24,7 @@ const DEFAULT_TIMEOUTS = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* `juno
|
|
27
|
+
* `juno task run` runs a command and reports it to a task's ping URL
|
|
28
28
|
* (plan-external-jobs-2-wrapper-and-tools.md, Decisions 3–5). Its first rule
|
|
29
29
|
* is transparency: the command gets the same stdin, and the wrapper's stdout,
|
|
30
30
|
* stderr and exit status are the command's, byte for byte — a scheduler such
|
|
@@ -34,11 +34,11 @@ const DEFAULT_TIMEOUTS = {
|
|
|
34
34
|
* on its own, so the wrapper never queues and never fails the command.
|
|
35
35
|
*
|
|
36
36
|
* It reads no Juno config and no API key; the URL is all it needs. argv is
|
|
37
|
-
* everything after `
|
|
37
|
+
* everything after `task run`, `--` included. The exit code is returned, not
|
|
38
38
|
* passed to process.exit, so stdout is flushed before the process ends.
|
|
39
39
|
* deps is injectable for the tests; the default binds the real ones.
|
|
40
40
|
*/
|
|
41
|
-
export async function
|
|
41
|
+
export async function taskRun(argv, io, deps = {}) {
|
|
42
42
|
const {
|
|
43
43
|
spawn = nodeSpawn,
|
|
44
44
|
fetch = globalThis.fetch,
|
|
@@ -65,20 +65,20 @@ export async function jobRun(argv, io, deps = {}) {
|
|
|
65
65
|
});
|
|
66
66
|
} catch (err) {
|
|
67
67
|
throw new UsageError(
|
|
68
|
-
`juno
|
|
68
|
+
`juno task run: ${err.message}\nusage: ${TASK_RUN_USAGE}`
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
if (command.length === 0) {
|
|
72
72
|
throw new UsageError(
|
|
73
|
-
`juno
|
|
73
|
+
`juno task run: missing -- <command>\nusage: ${TASK_RUN_USAGE}`
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
if (parsed.positionals.length > 1) {
|
|
77
77
|
throw new UsageError(
|
|
78
|
-
`juno
|
|
78
|
+
`juno task run: one URL before --, got ${parsed.positionals.length} arguments\nusage: ${TASK_RUN_USAGE}`
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
|
-
const url = await pingURL('
|
|
81
|
+
const url = await pingURL('task run', TASK_RUN_USAGE, {
|
|
82
82
|
positional: parsed.positionals[0],
|
|
83
83
|
file: parsed.values['url-file'],
|
|
84
84
|
env,
|
|
@@ -107,7 +107,11 @@ export async function jobRun(argv, io, deps = {}) {
|
|
|
107
107
|
let suffix;
|
|
108
108
|
let exitCode;
|
|
109
109
|
if (outcome.startError) {
|
|
110
|
-
|
|
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}`;
|
|
111
115
|
io.error(line);
|
|
112
116
|
err.push(Buffer.from(`${line}\n`));
|
|
113
117
|
suffix = 'fail';
|
|
@@ -136,14 +140,14 @@ export async function jobRun(argv, io, deps = {}) {
|
|
|
136
140
|
failure = await finish();
|
|
137
141
|
}
|
|
138
142
|
if (failure && verbose) {
|
|
139
|
-
io.error(`juno
|
|
143
|
+
io.error(`juno task run: could not report to Juno: ${failure}`);
|
|
140
144
|
}
|
|
141
145
|
return exitCode;
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
/**
|
|
145
149
|
* pingURL is the URL from the first source that yields one: the positional,
|
|
146
|
-
* then --url-file (its contents, trimmed), then $
|
|
150
|
+
* then --url-file (its contents, trimmed), then $JUNO_TASK_URL. The two
|
|
147
151
|
* indirect sources keep the token off the command line, where `ps` and the
|
|
148
152
|
* scheduler's job definition would show it. command and usage name the verb
|
|
149
153
|
* in its refusals.
|
|
@@ -165,26 +169,26 @@ async function pingURL(
|
|
|
165
169
|
}
|
|
166
170
|
if (text.trim()) return text.trim();
|
|
167
171
|
}
|
|
168
|
-
if (env.
|
|
172
|
+
if (env.JUNO_TASK_URL?.trim()) return env.JUNO_TASK_URL.trim();
|
|
169
173
|
throw new UsageError(
|
|
170
|
-
`juno ${command}: no ping URL — pass it, --url-file or
|
|
174
|
+
`juno ${command}: no ping URL — pass it, --url-file or JUNO_TASK_URL\nusage: ${usage}`
|
|
171
175
|
);
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
/**
|
|
175
179
|
* endpoint is `<url>/<suffix>?rid=<rid>`, used as given otherwise: no /ping/
|
|
176
|
-
* check, so a Healthchecks URL works too.
|
|
177
|
-
*
|
|
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.
|
|
178
183
|
*/
|
|
179
184
|
export function endpoint(url, suffix, rid) {
|
|
180
185
|
const q = url.indexOf('?');
|
|
181
|
-
const
|
|
186
|
+
const base = (q === -1 ? url : url.slice(0, q)).replace(/\/+$/, '');
|
|
187
|
+
const path = suffix ? `${base}/${suffix}` : base;
|
|
182
188
|
const params = [];
|
|
183
189
|
if (q !== -1 && q < url.length - 1) params.push(url.slice(q + 1));
|
|
184
190
|
if (rid) params.push(`rid=${encodeURIComponent(rid)}`);
|
|
185
|
-
return params.length
|
|
186
|
-
? `${path}/${suffix}?${params.join('&')}`
|
|
187
|
-
: `${path}/${suffix}`;
|
|
191
|
+
return params.length ? `${path}?${params.join('&')}` : path;
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
/**
|
|
@@ -291,24 +295,24 @@ export class Tail {
|
|
|
291
295
|
}
|
|
292
296
|
}
|
|
293
297
|
|
|
294
|
-
export const
|
|
295
|
-
'juno
|
|
298
|
+
export const TASK_PING_USAGE =
|
|
299
|
+
'juno task ping [<url>] [--url-file <path>] [--start | --fail | --exit <0-255> | --log] [--rid <id>]';
|
|
296
300
|
|
|
297
301
|
const PING_KINDS = ['start', 'fail', 'exit', 'log'];
|
|
298
302
|
|
|
299
303
|
/**
|
|
300
|
-
* `juno
|
|
301
|
-
* `
|
|
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
|
|
302
306
|
* and finish happen in different places. Without a flag it reports success;
|
|
303
307
|
* `--start`, `--fail`, `--exit <n>` and `--log` send the other reports, and
|
|
304
308
|
* `--rid` pairs a start with its finish. It sends no output.
|
|
305
309
|
*
|
|
306
|
-
* It finds the URL as `
|
|
307
|
-
* API key. Unlike `
|
|
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
|
|
308
312
|
* fails after one retry is exit 1 with the reason, not silence: the caller
|
|
309
313
|
* asked for exactly this and can `|| true` it.
|
|
310
314
|
*/
|
|
311
|
-
export async function
|
|
315
|
+
export async function taskPing(args, io, deps = {}) {
|
|
312
316
|
const {
|
|
313
317
|
fetch = globalThis.fetch,
|
|
314
318
|
env = process.env,
|
|
@@ -316,7 +320,7 @@ export async function jobPing(args, io, deps = {}) {
|
|
|
316
320
|
timeouts = DEFAULT_TIMEOUTS,
|
|
317
321
|
} = deps;
|
|
318
322
|
const usage = (msg) =>
|
|
319
|
-
new UsageError(`juno
|
|
323
|
+
new UsageError(`juno task ping: ${msg}\nusage: ${TASK_PING_USAGE}`);
|
|
320
324
|
|
|
321
325
|
if (args._.length > 1) {
|
|
322
326
|
throw usage(`one URL, got ${args._.length} arguments`);
|
|
@@ -327,7 +331,8 @@ export async function jobPing(args, io, deps = {}) {
|
|
|
327
331
|
if (kinds.length > 1) {
|
|
328
332
|
throw usage(`--${kinds.join(' and --')} are different reports; pick one`);
|
|
329
333
|
}
|
|
330
|
-
|
|
334
|
+
// A success is the bare URL: the protocol has no /success suffix.
|
|
335
|
+
let suffix = '';
|
|
331
336
|
if (kinds[0] === 'exit') {
|
|
332
337
|
const n = args.exit.trim();
|
|
333
338
|
if (!/^\d{1,3}$/.test(n) || Number(n) > 255) {
|
|
@@ -337,7 +342,7 @@ export async function jobPing(args, io, deps = {}) {
|
|
|
337
342
|
} else if (kinds[0]) {
|
|
338
343
|
suffix = kinds[0];
|
|
339
344
|
}
|
|
340
|
-
const url = await pingURL('
|
|
345
|
+
const url = await pingURL('task ping', TASK_PING_USAGE, {
|
|
341
346
|
positional: args._[0],
|
|
342
347
|
file: args['url-file'],
|
|
343
348
|
env,
|
|
@@ -356,7 +361,7 @@ export async function jobPing(args, io, deps = {}) {
|
|
|
356
361
|
failure = await send();
|
|
357
362
|
}
|
|
358
363
|
if (failure) {
|
|
359
|
-
io.error(`juno
|
|
364
|
+
io.error(`juno task ping: could not report to Juno: ${failure}`);
|
|
360
365
|
return EXIT_FAILURE;
|
|
361
366
|
}
|
|
362
367
|
return EXIT_OK;
|
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
|
|
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
|
@@ -30,11 +30,11 @@ 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
32
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} from './commands/
|
|
33
|
+
TASK_PING_USAGE,
|
|
34
|
+
TASK_RUN_USAGE,
|
|
35
|
+
taskPing,
|
|
36
|
+
taskRun,
|
|
37
|
+
} from './commands/task-run.js';
|
|
38
38
|
import { PULL_USAGE, pull } from './commands/pull.js';
|
|
39
39
|
import { SKILL_USAGE, skill } from './commands/skill.js';
|
|
40
40
|
import {
|
|
@@ -111,19 +111,19 @@ const NON_TOOL = {
|
|
|
111
111
|
// raw: everything after `--` is the wrapped command's, verbatim, so the
|
|
112
112
|
// argv goes to the handler unparsed (see runRow). No --url or --token: the
|
|
113
113
|
// wrapper reads no Juno config, only the ping URL.
|
|
114
|
-
'
|
|
115
|
-
run: (argv, io) =>
|
|
114
|
+
'task run': {
|
|
115
|
+
run: (argv, io) => taskRun(argv, io, { env: io.env }),
|
|
116
116
|
options: {
|
|
117
117
|
'url-file': { type: 'string' },
|
|
118
118
|
verbose: { type: 'boolean', default: false },
|
|
119
119
|
},
|
|
120
|
-
usage:
|
|
120
|
+
usage: TASK_RUN_USAGE,
|
|
121
121
|
raw: true,
|
|
122
122
|
},
|
|
123
|
-
// No --url or --token either: one report to the ping URL, found as
|
|
123
|
+
// No --url or --token either: one report to the ping URL, found as task run
|
|
124
124
|
// finds it.
|
|
125
|
-
'
|
|
126
|
-
run: (args, io) =>
|
|
125
|
+
'task ping': {
|
|
126
|
+
run: (args, io) => taskPing(args, io, { env: io.env }),
|
|
127
127
|
options: {
|
|
128
128
|
'url-file': { type: 'string' },
|
|
129
129
|
start: { type: 'boolean' },
|
|
@@ -132,7 +132,7 @@ const NON_TOOL = {
|
|
|
132
132
|
log: { type: 'boolean' },
|
|
133
133
|
rid: { type: 'string' },
|
|
134
134
|
},
|
|
135
|
-
usage:
|
|
135
|
+
usage: TASK_PING_USAGE,
|
|
136
136
|
},
|
|
137
137
|
'config set': { run: configSet, options: {}, usage: CONFIG_SET_USAGE },
|
|
138
138
|
'agent-skill install': {
|
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,10 +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": "
|
|
225
|
-
{ "command": "
|
|
226
|
-
{ "command": "
|
|
227
|
-
{ "command": "
|
|
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" },
|
|
228
255
|
{ "command": "credential list", "tool": "list_credentials" },
|
|
229
256
|
{ "command": "person list", "tool": "list_people" },
|
|
230
257
|
{ "command": "person get", "tool": "get_person", "id_field": "id" },
|
|
@@ -240,6 +267,16 @@
|
|
|
240
267
|
"id_field": "run_id",
|
|
241
268
|
"flags": { "handle": { "field": "handle_id", "type": "string" } }
|
|
242
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
|
+
},
|
|
243
280
|
{
|
|
244
281
|
"command": "ha get-entity",
|
|
245
282
|
"tool": "get_entity",
|
|
@@ -276,14 +313,14 @@
|
|
|
276
313
|
"summary": "Approve the tool call an assistant session is waiting on"
|
|
277
314
|
},
|
|
278
315
|
{
|
|
279
|
-
"command": "
|
|
316
|
+
"command": "task run",
|
|
280
317
|
"effect": "act",
|
|
281
|
-
"summary": "Run a command and report it to
|
|
318
|
+
"summary": "Run a command and report it to a task's ping URL"
|
|
282
319
|
},
|
|
283
320
|
{
|
|
284
|
-
"command": "
|
|
321
|
+
"command": "task ping",
|
|
285
322
|
"effect": "act",
|
|
286
|
-
"summary": "Send one report (success, start, fail, an exit status or a log) to
|
|
323
|
+
"summary": "Send one report (success, start, fail, an exit status or a log) to a task's ping URL"
|
|
287
324
|
},
|
|
288
325
|
{
|
|
289
326
|
"command": "credential set",
|