@junoflow/cli 0.14.0 → 0.17.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 +310 -117
- package/app-client.js +95 -9
- package/package.json +2 -2
- package/src/api.js +154 -15
- package/src/authoring-skills.js +2 -6
- package/src/commands/app-init.js +1 -1
- package/src/commands/approval-wait.js +215 -0
- package/src/commands/flow-init.js +1 -1
- package/src/commands/init.js +1 -1
- package/src/commands/permissions.js +47 -0
- package/src/commands/run-follow.js +68 -0
- package/src/commands/session-approve.js +22 -3
- package/src/commands/session.js +103 -0
- package/src/commands/skill.js +28 -19
- package/src/commands/{job-run.js → task-run.js} +36 -31
- package/src/commands/tool.js +23 -5
- package/src/commands/whoami.js +38 -0
- package/src/exit.js +6 -2
- package/src/help.js +143 -22
- package/src/index.js +127 -60
- package/src/prompt.js +9 -0
- package/src/verbs.js +6 -1
- package/verbs.json +435 -13
- package/src/commands/agent-skill-install.js +0 -64
- package/src/installed-skill.js +0 -55
package/src/help.js
CHANGED
|
@@ -50,12 +50,18 @@ export async function groupHelp(group, rows, handlerFor, args, io) {
|
|
|
50
50
|
const authoring = Object.hasOwn(AUTHORING_HEADINGS, group) ? group : null;
|
|
51
51
|
let tools = null;
|
|
52
52
|
let skills = null;
|
|
53
|
+
let readFirst = [];
|
|
53
54
|
if (onInstance.some((row) => row.tool) || authoring) {
|
|
54
55
|
try {
|
|
55
56
|
const { client } = await connect(args, io);
|
|
56
57
|
if (onInstance.some((row) => row.tool)) {
|
|
57
58
|
tools = new Map((await client.listTools()).map((t) => [t.name, t]));
|
|
58
59
|
}
|
|
60
|
+
readFirst = await readFirstFor(
|
|
61
|
+
client,
|
|
62
|
+
[group, ...members.map((r) => r.tool)],
|
|
63
|
+
io
|
|
64
|
+
);
|
|
59
65
|
if (authoring) {
|
|
60
66
|
skills = (await client.listSkills({ authoring }))?.skills ?? [];
|
|
61
67
|
}
|
|
@@ -65,19 +71,17 @@ export async function groupHelp(group, rows, handlerFor, args, io) {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
const lines = members.map((row) => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
: handlerFor(row).usage;
|
|
71
|
-
if (!row.tool) return [synopsis, row.effect, row.summary];
|
|
74
|
+
const line = synopsis(row, handlerFor);
|
|
75
|
+
if (!row.tool) return [line, row.effect, row.summary];
|
|
72
76
|
const tool = tools?.get(row.tool);
|
|
77
|
+
// A row that fixes part of its tool's input (one verb of command_task)
|
|
78
|
+
// says what it does itself; the tool's first sentence covers every verb.
|
|
79
|
+
if (row.summary)
|
|
80
|
+
return [line, row.effect ?? tool?.effect ?? '', row.summary];
|
|
73
81
|
// Offline, no tool row shows an effect: printing only the rows that carry
|
|
74
82
|
// their own would read as if the others had none.
|
|
75
|
-
if (!tool) return [
|
|
76
|
-
return [
|
|
77
|
-
synopsis,
|
|
78
|
-
row.effect ?? tool.effect,
|
|
79
|
-
firstSentence(tool.description),
|
|
80
|
-
];
|
|
83
|
+
if (!tool) return [line, '', ''];
|
|
84
|
+
return [line, row.effect ?? tool.effect, firstSentence(tool.description)];
|
|
81
85
|
});
|
|
82
86
|
// One column width across both headings, so the two lists line up.
|
|
83
87
|
const render = tabulate(lines);
|
|
@@ -89,6 +93,7 @@ export async function groupHelp(group, rows, handlerFor, args, io) {
|
|
|
89
93
|
: `usage: juno ${group} <verb>`,
|
|
90
94
|
'',
|
|
91
95
|
];
|
|
96
|
+
if (readFirst.length) out.push(readFirstLine(readFirst), '');
|
|
92
97
|
if (inWorkingCopy.length === 0) {
|
|
93
98
|
out.push(...body(members));
|
|
94
99
|
} else {
|
|
@@ -109,21 +114,99 @@ export async function groupHelp(group, rows, handlerFor, args, io) {
|
|
|
109
114
|
return out.join('\n');
|
|
110
115
|
}
|
|
111
116
|
|
|
112
|
-
export const WORKING_COPY_HEADING =
|
|
113
|
-
'In a working copy (run from the project directory)';
|
|
114
|
-
|
|
115
117
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
+
* rootHelp is `juno --help` and bare `juno`: the place an agent with nothing
|
|
119
|
+
* installed starts. Usage first, then the primer from GET /api/agent-primer
|
|
120
|
+
* (what Juno is made of, how to work with it, what only the user does), then
|
|
121
|
+
* every group and top-level command by one line from verbs.json, the
|
|
122
|
+
* working-copy commands, and the skill index from GET /api/skills. The
|
|
123
|
+
* instance owns the primer and the skills; the CLI owns the listing.
|
|
124
|
+
*
|
|
125
|
+
* Offline — no instance configured, unreachable, refused — the primer and the
|
|
126
|
+
* skills are dropped, the reason goes to stderr as for a group's help, and one
|
|
127
|
+
* line says how to connect. A help must not fail offline.
|
|
118
128
|
*/
|
|
119
|
-
export function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
export async function rootHelp(rows, handlerFor, groupSummaries, args, io) {
|
|
130
|
+
let primer = null;
|
|
131
|
+
let skills = null;
|
|
132
|
+
try {
|
|
133
|
+
const { client } = await connect(args, io);
|
|
134
|
+
const [p, index] = await Promise.all([
|
|
135
|
+
client.getAgentPrimer(),
|
|
136
|
+
client.listSkills(),
|
|
137
|
+
]);
|
|
138
|
+
primer = p?.body ?? null;
|
|
139
|
+
skills = index?.skills ?? [];
|
|
140
|
+
} catch (err) {
|
|
141
|
+
io.warn(`(guide unavailable: ${err.message})`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const out = [
|
|
145
|
+
'juno — operate a Juno instance, and develop flows, integrations and apps for it',
|
|
146
|
+
'',
|
|
147
|
+
'Usage: juno <command> [options]',
|
|
148
|
+
` juno <group> <verb> [id] ${INPUT_USAGE}`,
|
|
149
|
+
'',
|
|
150
|
+
];
|
|
151
|
+
if (primer) {
|
|
152
|
+
out.push(primer.trim(), '');
|
|
153
|
+
} else {
|
|
154
|
+
out.push(
|
|
155
|
+
'The guide to working with Juno and the skill index come from the instance,',
|
|
156
|
+
'which did not answer (the reason is on stderr). `juno config set url <url>`',
|
|
157
|
+
'and `juno config set token <key>` connect this CLI to one.',
|
|
158
|
+
''
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const groups = [];
|
|
163
|
+
for (const row of rows) {
|
|
164
|
+
const group = groupOf(row);
|
|
165
|
+
if (group && !groups.includes(group)) groups.push(group);
|
|
166
|
+
}
|
|
167
|
+
out.push("Groups (run `juno <group> --help` for a group's commands):");
|
|
168
|
+
out.push(...tabulate(groups.map((g) => [g, '', groupSummaries[g] ?? ''])));
|
|
169
|
+
out.push('', 'Commands:');
|
|
170
|
+
out.push(
|
|
171
|
+
...tabulate(
|
|
172
|
+
rows
|
|
173
|
+
.filter((row) => !groupOf(row))
|
|
174
|
+
.map((row) => [synopsis(row, handlerFor), '', row.summary ?? ''])
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (skills?.length) {
|
|
179
|
+
out.push(
|
|
180
|
+
'',
|
|
181
|
+
'Skills (`juno skill <name>...` prints them; `juno skill` gives each its full description):'
|
|
182
|
+
);
|
|
183
|
+
const width = Math.max(...skills.map((s) => s.name.length));
|
|
184
|
+
for (const s of skills) {
|
|
185
|
+
out.push(` ${s.name.padEnd(width)} ${firstSentence(s.description)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
out.push(
|
|
190
|
+
'',
|
|
191
|
+
'--url <url> picks the instance; else $JUNO_URL, package.json juno.target,',
|
|
192
|
+
'then the user config (juno config set url <url>).',
|
|
193
|
+
'--token <key> is the API key to send; else $JUNO_TOKEN, then the user config',
|
|
194
|
+
'(juno config set token <key>). Create one under Settings → API keys.',
|
|
195
|
+
''
|
|
196
|
+
);
|
|
197
|
+
return out.join('\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** One row's synopsis as a listing shows it: `juno state get <id>`. */
|
|
201
|
+
function synopsis(row, handlerFor) {
|
|
202
|
+
if (!row.tool) return handlerFor(row).usage;
|
|
203
|
+
const positional = positionalName(row);
|
|
204
|
+
return `juno ${row.command}${positional ? ` ${positional}` : ''}`;
|
|
125
205
|
}
|
|
126
206
|
|
|
207
|
+
export const WORKING_COPY_HEADING =
|
|
208
|
+
'In a working copy (run from the project directory)';
|
|
209
|
+
|
|
127
210
|
/** Rows of [synopsis, effect, text] as aligned, indented lines. */
|
|
128
211
|
function tabulate(lines) {
|
|
129
212
|
const width = Math.max(...lines.map(([s]) => s.length));
|
|
@@ -149,7 +232,15 @@ export async function verbHelp(row, args, io) {
|
|
|
149
232
|
);
|
|
150
233
|
}
|
|
151
234
|
|
|
152
|
-
const out = [`usage: ${rowUsage(row)}`, ''
|
|
235
|
+
const out = [`usage: ${rowUsage(row)}`, ''];
|
|
236
|
+
// Which skills to read first is the server's mapping (each skill's
|
|
237
|
+
// applies_to); a failure to fetch it drops the line, never the help.
|
|
238
|
+
const readFirst = await readFirstFor(client, [groupOf(row), row.tool], io);
|
|
239
|
+
if (readFirst.length) out.push(readFirstLine(readFirst), '');
|
|
240
|
+
// A top-level command's summary is its line in `juno --help`; the tool's
|
|
241
|
+
// description says the same at length.
|
|
242
|
+
if (row.summary && row.fixed_input) out.push(row.summary, '');
|
|
243
|
+
out.push(tool.description.trim(), '');
|
|
153
244
|
out.push(`effect: ${row.effect ?? tool.effect}`);
|
|
154
245
|
out.push(`tool: ${row.tool}`);
|
|
155
246
|
if (row.id_field) {
|
|
@@ -177,6 +268,36 @@ export async function verbHelp(row, args, io) {
|
|
|
177
268
|
return out.join('\n');
|
|
178
269
|
}
|
|
179
270
|
|
|
271
|
+
/**
|
|
272
|
+
* skillsFor is the names of the skills whose applies_to names any of `names`
|
|
273
|
+
* (a group, a tool), in the index's order. The mapping is the server's: the CLI
|
|
274
|
+
* only matches the strings.
|
|
275
|
+
*/
|
|
276
|
+
export function skillsFor(skills, names) {
|
|
277
|
+
const wanted = new Set(names.filter(Boolean));
|
|
278
|
+
return skills
|
|
279
|
+
.filter((s) => (s.applies_to ?? []).some((n) => wanted.has(n)))
|
|
280
|
+
.map((s) => s.name);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* readFirstFor fetches the skill index and answers skillsFor over it. Its
|
|
285
|
+
* own failure costs only the line: the reason goes to stderr and the help
|
|
286
|
+
* goes on.
|
|
287
|
+
*/
|
|
288
|
+
async function readFirstFor(client, names, io) {
|
|
289
|
+
try {
|
|
290
|
+
return skillsFor((await client.listSkills())?.skills ?? [], names);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
io.warn(`(skills unavailable: ${err.message})`);
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function readFirstLine(names) {
|
|
298
|
+
return `Read first: juno skill ${names.join(' ')}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
180
301
|
/** The help of a non-tool command, offline: its summary and its usage. */
|
|
181
302
|
export function nonToolHelp(row, handler) {
|
|
182
303
|
return `${row.summary}\n\nusage: ${handler.usage}`;
|
package/src/index.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
AGENT_SKILL_INSTALL_USAGE,
|
|
5
|
-
agentSkillInstall,
|
|
6
|
-
} from './commands/agent-skill-install.js';
|
|
7
3
|
import { APP_CHECK_USAGE, appCheck } from './commands/app-check.js';
|
|
8
4
|
import { APP_DEPLOY_USAGE, appDeploy } from './commands/app-deploy.js';
|
|
9
5
|
import { APP_DEV_USAGE, appDev } from './commands/app-dev.js';
|
|
10
6
|
import { APP_GEN_TYPES_USAGE, appGenTypes } from './commands/app-gen-types.js';
|
|
11
7
|
import { APP_INIT_USAGE, appInit } from './commands/app-init.js';
|
|
12
8
|
import { APP_PULL_USAGE, appPull } from './commands/app-pull.js';
|
|
9
|
+
import { APPROVAL_WAIT_USAGE, approvalWait } from './commands/approval-wait.js';
|
|
13
10
|
import { ASK_USAGE, ask } from './commands/ask.js';
|
|
14
11
|
import { CHECK_USAGE, check } from './commands/check.js';
|
|
15
12
|
import { CONFIG_SET_USAGE, configSet } from './commands/config.js';
|
|
@@ -30,19 +27,39 @@ import { FLOW_PULL_USAGE, flowPull } from './commands/flow-pull.js';
|
|
|
30
27
|
import { GEN_TYPES_USAGE, genTypes } from './commands/gen-types.js';
|
|
31
28
|
import { INIT_USAGE, init } from './commands/init.js';
|
|
32
29
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} from './commands/
|
|
30
|
+
TASK_PING_USAGE,
|
|
31
|
+
TASK_RUN_USAGE,
|
|
32
|
+
taskPing,
|
|
33
|
+
taskRun,
|
|
34
|
+
} from './commands/task-run.js';
|
|
38
35
|
import { PULL_USAGE, pull } from './commands/pull.js';
|
|
36
|
+
import { PERMISSIONS_USAGE, permissions } from './commands/permissions.js';
|
|
39
37
|
import { SKILL_USAGE, skill } from './commands/skill.js';
|
|
40
38
|
import {
|
|
41
39
|
SESSION_APPROVE_USAGE,
|
|
42
40
|
sessionApprove,
|
|
43
41
|
} from './commands/session-approve.js';
|
|
42
|
+
import { runFollow } from './commands/run-follow.js';
|
|
43
|
+
import {
|
|
44
|
+
SESSION_CANCEL_USAGE,
|
|
45
|
+
SESSION_EXPORT_USAGE,
|
|
46
|
+
SESSION_GET_USAGE,
|
|
47
|
+
SESSION_LIST_USAGE,
|
|
48
|
+
SESSION_RATE_USAGE,
|
|
49
|
+
sessionCancel,
|
|
50
|
+
sessionExport,
|
|
51
|
+
sessionGet,
|
|
52
|
+
sessionList,
|
|
53
|
+
sessionRate,
|
|
54
|
+
} from './commands/session.js';
|
|
44
55
|
import { runTool } from './commands/tool.js';
|
|
45
56
|
import { VERIFY_TYPES_USAGE, verifyTypes } from './commands/verify-types.js';
|
|
57
|
+
import {
|
|
58
|
+
VERSION_USAGE,
|
|
59
|
+
WHOAMI_USAGE,
|
|
60
|
+
version,
|
|
61
|
+
whoami,
|
|
62
|
+
} from './commands/whoami.js';
|
|
46
63
|
import {
|
|
47
64
|
ConflictError,
|
|
48
65
|
DiagnosticsError,
|
|
@@ -58,15 +75,15 @@ import {
|
|
|
58
75
|
EXIT_UNREACHABLE,
|
|
59
76
|
EXIT_USAGE,
|
|
60
77
|
} from './exit.js';
|
|
61
|
-
import {
|
|
62
|
-
groupHelp,
|
|
63
|
-
nonToolHelp,
|
|
64
|
-
verbHelp,
|
|
65
|
-
WORKING_COPY_HEADING,
|
|
66
|
-
workingCopyListing,
|
|
67
|
-
} from './help.js';
|
|
78
|
+
import { groupHelp, nonToolHelp, rootHelp, verbHelp } from './help.js';
|
|
68
79
|
import { closest, closestByVerb, levenshtein } from './suggest.js';
|
|
69
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
groupOf,
|
|
82
|
+
groupSummaries,
|
|
83
|
+
normalizeVerb,
|
|
84
|
+
rowFor,
|
|
85
|
+
rows,
|
|
86
|
+
} from './verbs.js';
|
|
70
87
|
|
|
71
88
|
/**
|
|
72
89
|
* The two flags every command that reaches an instance takes, and the two a
|
|
@@ -94,11 +111,58 @@ const NON_TOOL = {
|
|
|
94
111
|
},
|
|
95
112
|
usage: ASK_USAGE,
|
|
96
113
|
},
|
|
114
|
+
whoami: { run: whoami, options: URL_OPTION, usage: WHOAMI_USAGE },
|
|
115
|
+
version: { run: version, options: URL_OPTION, usage: VERSION_USAGE },
|
|
116
|
+
permissions: {
|
|
117
|
+
run: permissions,
|
|
118
|
+
options: {
|
|
119
|
+
...URL_OPTION,
|
|
120
|
+
'allow-change': { type: 'boolean', default: false },
|
|
121
|
+
'working-copy': { type: 'boolean', default: false },
|
|
122
|
+
json: { type: 'boolean', default: false },
|
|
123
|
+
},
|
|
124
|
+
usage: PERMISSIONS_USAGE,
|
|
125
|
+
},
|
|
126
|
+
'session list': {
|
|
127
|
+
run: sessionList,
|
|
128
|
+
options: {
|
|
129
|
+
...URL_OPTION,
|
|
130
|
+
rating: { type: 'string' },
|
|
131
|
+
profile: { type: 'string' },
|
|
132
|
+
limit: { type: 'string' },
|
|
133
|
+
},
|
|
134
|
+
usage: SESSION_LIST_USAGE,
|
|
135
|
+
},
|
|
136
|
+
'session get': {
|
|
137
|
+
run: sessionGet,
|
|
138
|
+
options: { ...URL_OPTION, transcript: { type: 'boolean', default: false } },
|
|
139
|
+
usage: SESSION_GET_USAGE,
|
|
140
|
+
},
|
|
141
|
+
'session export': {
|
|
142
|
+
run: sessionExport,
|
|
143
|
+
options: URL_OPTION,
|
|
144
|
+
usage: SESSION_EXPORT_USAGE,
|
|
145
|
+
},
|
|
146
|
+
'session rate': {
|
|
147
|
+
run: sessionRate,
|
|
148
|
+
options: { ...URL_OPTION, rating: { type: 'string' } },
|
|
149
|
+
usage: SESSION_RATE_USAGE,
|
|
150
|
+
},
|
|
151
|
+
'session cancel': {
|
|
152
|
+
run: sessionCancel,
|
|
153
|
+
options: URL_OPTION,
|
|
154
|
+
usage: SESSION_CANCEL_USAGE,
|
|
155
|
+
},
|
|
97
156
|
'session approve': {
|
|
98
157
|
run: sessionApprove,
|
|
99
158
|
options: { ...URL_OPTION, wait: { type: 'string' } },
|
|
100
159
|
usage: SESSION_APPROVE_USAGE,
|
|
101
160
|
},
|
|
161
|
+
'approval wait': {
|
|
162
|
+
run: approvalWait,
|
|
163
|
+
options: { ...URL_OPTION, wait: { type: 'string' } },
|
|
164
|
+
usage: APPROVAL_WAIT_USAGE,
|
|
165
|
+
},
|
|
102
166
|
'credential set': {
|
|
103
167
|
run: credentialSet,
|
|
104
168
|
options: {
|
|
@@ -111,19 +175,19 @@ const NON_TOOL = {
|
|
|
111
175
|
// raw: everything after `--` is the wrapped command's, verbatim, so the
|
|
112
176
|
// argv goes to the handler unparsed (see runRow). No --url or --token: the
|
|
113
177
|
// wrapper reads no Juno config, only the ping URL.
|
|
114
|
-
'
|
|
115
|
-
run: (argv, io) =>
|
|
178
|
+
'task run': {
|
|
179
|
+
run: (argv, io) => taskRun(argv, io, { env: io.env }),
|
|
116
180
|
options: {
|
|
117
181
|
'url-file': { type: 'string' },
|
|
118
182
|
verbose: { type: 'boolean', default: false },
|
|
119
183
|
},
|
|
120
|
-
usage:
|
|
184
|
+
usage: TASK_RUN_USAGE,
|
|
121
185
|
raw: true,
|
|
122
186
|
},
|
|
123
|
-
// No --url or --token either: one report to the ping URL, found as
|
|
187
|
+
// No --url or --token either: one report to the ping URL, found as task run
|
|
124
188
|
// finds it.
|
|
125
|
-
'
|
|
126
|
-
run: (args, io) =>
|
|
189
|
+
'task ping': {
|
|
190
|
+
run: (args, io) => taskPing(args, io, { env: io.env }),
|
|
127
191
|
options: {
|
|
128
192
|
'url-file': { type: 'string' },
|
|
129
193
|
start: { type: 'boolean' },
|
|
@@ -132,21 +196,9 @@ const NON_TOOL = {
|
|
|
132
196
|
log: { type: 'boolean' },
|
|
133
197
|
rid: { type: 'string' },
|
|
134
198
|
},
|
|
135
|
-
usage:
|
|
199
|
+
usage: TASK_PING_USAGE,
|
|
136
200
|
},
|
|
137
201
|
'config set': { run: configSet, options: {}, usage: CONFIG_SET_USAGE },
|
|
138
|
-
'agent-skill install': {
|
|
139
|
-
run: agentSkillInstall,
|
|
140
|
-
options: {
|
|
141
|
-
...URL_OPTION,
|
|
142
|
-
dir: { type: 'string' },
|
|
143
|
-
'allow-change': { type: 'boolean', default: false },
|
|
144
|
-
'working-copy': { type: 'boolean', default: false },
|
|
145
|
-
'agents-md': { type: 'boolean', default: false },
|
|
146
|
-
json: { type: 'boolean', default: false },
|
|
147
|
-
},
|
|
148
|
-
usage: AGENT_SKILL_INSTALL_USAGE,
|
|
149
|
-
},
|
|
150
202
|
};
|
|
151
203
|
|
|
152
204
|
/**
|
|
@@ -301,6 +353,21 @@ const WORKING_COPY = {
|
|
|
301
353
|
},
|
|
302
354
|
};
|
|
303
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Flags a tool row takes beyond its verbs.json ones, which name no field of
|
|
358
|
+
* the tool's input and so cannot be in the table: what the CLI does around
|
|
359
|
+
* the call. `run get --follow` streams the run's steps to stderr until it is
|
|
360
|
+
* done, then prints get_run as without it.
|
|
361
|
+
*/
|
|
362
|
+
const TOOL_EXTRAS = {
|
|
363
|
+
'run get': {
|
|
364
|
+
options: { follow: { type: 'boolean', default: false } },
|
|
365
|
+
help: "flag: --follow → no input field: stream the run's steps to stderr until it ends, then print the run",
|
|
366
|
+
run: (row, args, io) =>
|
|
367
|
+
args.follow ? runFollow(row, args, io) : runTool(row, args, io),
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
304
371
|
/** The handler of a non-tool row, working-copy or not, or undefined. */
|
|
305
372
|
function handlerFor(row) {
|
|
306
373
|
if (row.tool) return undefined;
|
|
@@ -320,26 +387,6 @@ function dispatchable(command) {
|
|
|
320
387
|
return row && DISPATCHABLE.includes(row) ? row : undefined;
|
|
321
388
|
}
|
|
322
389
|
|
|
323
|
-
const USAGE = `juno — operate a Juno instance, and develop flows, integrations and apps for it
|
|
324
|
-
|
|
325
|
-
Usage: juno <command> [options]
|
|
326
|
-
juno <group> <verb> [id] [--input <json>|-|@file] [--jq <expr>]
|
|
327
|
-
|
|
328
|
-
Groups: ${[...GROUPS].join(', ')}
|
|
329
|
-
Commands: ${DISPATCHABLE.filter((r) => !groupOf(r))
|
|
330
|
-
.map((r) => r.command)
|
|
331
|
-
.join(', ')}
|
|
332
|
-
Run \`juno <group> --help\` for a group's commands.
|
|
333
|
-
|
|
334
|
-
${WORKING_COPY_HEADING}:
|
|
335
|
-
${workingCopyListing(DISPATCHABLE, handlerFor)}
|
|
336
|
-
|
|
337
|
-
--url <url> picks the instance; else $JUNO_URL, package.json juno.target,
|
|
338
|
-
then the user config (juno config set url <url>).
|
|
339
|
-
--token <key> is the API key to send; else $JUNO_TOKEN, then the user config
|
|
340
|
-
(juno config set token <key>). Create one under Settings → API keys.
|
|
341
|
-
`;
|
|
342
|
-
|
|
343
390
|
/**
|
|
344
391
|
* run is the whole CLI, injectable so the tests drive it without spawning a
|
|
345
392
|
* process: io carries cwd, env, the TTY bit, stdin and the three output
|
|
@@ -387,9 +434,11 @@ async function dispatch(argv, io) {
|
|
|
387
434
|
const { leading, rest } = leadingFlags(argv);
|
|
388
435
|
const [first, ...tail] = rest;
|
|
389
436
|
|
|
437
|
+
// Bare `juno` is `juno --help`, exit 0: it is where an agent with nothing
|
|
438
|
+
// installed starts, and the primer is the answer it came for.
|
|
390
439
|
if (!first || first === '--help' || first === '-h' || first === 'help') {
|
|
391
|
-
|
|
392
|
-
return
|
|
440
|
+
const show = (args) => showRootHelp(args, io);
|
|
441
|
+
return runCommand({ options: URL_OPTION }, tail, leading, io, show, show);
|
|
393
442
|
}
|
|
394
443
|
|
|
395
444
|
const top = dispatchable(first);
|
|
@@ -428,6 +477,11 @@ async function dispatch(argv, io) {
|
|
|
428
477
|
throw unknownTopLevel(first, first, [first]);
|
|
429
478
|
}
|
|
430
479
|
|
|
480
|
+
async function showRootHelp(args, io) {
|
|
481
|
+
io.log(await rootHelp(DISPATCHABLE, handlerFor, groupSummaries, args, io));
|
|
482
|
+
return EXIT_OK;
|
|
483
|
+
}
|
|
484
|
+
|
|
431
485
|
async function showGroupHelp(group, args, io, code) {
|
|
432
486
|
io.log(await groupHelp(group, DISPATCHABLE, handlerFor, args, io));
|
|
433
487
|
return code;
|
|
@@ -468,21 +522,28 @@ function runRow(row, argv, leading, io) {
|
|
|
468
522
|
);
|
|
469
523
|
}
|
|
470
524
|
|
|
525
|
+
// --wait is how long a destructive tool's approval is waited for; every
|
|
526
|
+
// tool row takes it, since which tools are destructive is the server's.
|
|
471
527
|
const options = {
|
|
472
528
|
...URL_OPTION,
|
|
473
529
|
input: { type: 'string' },
|
|
474
530
|
jq: { type: 'string' },
|
|
531
|
+
wait: { type: 'string' },
|
|
475
532
|
};
|
|
476
533
|
for (const [flag, spec] of Object.entries(row.flags ?? {})) {
|
|
477
534
|
options[flag] = { type: 'string', multiple: spec.multiple === true };
|
|
478
535
|
}
|
|
536
|
+
const extras = TOOL_EXTRAS[row.command];
|
|
537
|
+
Object.assign(options, extras?.options);
|
|
538
|
+
const run = extras?.run ?? runTool;
|
|
479
539
|
return runCommand(
|
|
480
|
-
{ run: (args) =>
|
|
540
|
+
{ run: (args) => run(row, args, io), options },
|
|
481
541
|
argv,
|
|
482
542
|
leading,
|
|
483
543
|
io,
|
|
484
544
|
async (args) => {
|
|
485
|
-
|
|
545
|
+
const help = await verbHelp(row, args, io);
|
|
546
|
+
io.log(extras?.help ? `${help}\n${extras.help}` : help);
|
|
486
547
|
return EXIT_OK;
|
|
487
548
|
}
|
|
488
549
|
);
|
|
@@ -611,6 +672,12 @@ export function defaultIO() {
|
|
|
611
672
|
write: (text) => process.stdout.write(text),
|
|
612
673
|
warn: (msg) => process.stderr.write(`${msg}\n`),
|
|
613
674
|
error: (msg) => process.stderr.write(`${msg}\n`),
|
|
675
|
+
// Ctrl-C while waiting for an approval: the handler says how to resume,
|
|
676
|
+
// and the wait returns rather than the process dying mid-line.
|
|
677
|
+
onInterrupt: (fn) => {
|
|
678
|
+
process.once('SIGINT', fn);
|
|
679
|
+
return () => process.off('SIGINT', fn);
|
|
680
|
+
},
|
|
614
681
|
};
|
|
615
682
|
}
|
|
616
683
|
|
package/src/prompt.js
CHANGED
|
@@ -182,6 +182,15 @@ function nextStep(event, id) {
|
|
|
182
182
|
return `still running; keep waiting with: juno ask --session ${id}`;
|
|
183
183
|
}
|
|
184
184
|
if (event.type !== 'awaiting_input') return null;
|
|
185
|
+
const approvalID = event.pending_input?.approval_id;
|
|
186
|
+
if (approvalID) {
|
|
187
|
+
// A destructive call's approval record: only a person in Juno answers it.
|
|
188
|
+
const url = event.pending_input.approval_url ?? `/approvals/${approvalID}`;
|
|
189
|
+
return (
|
|
190
|
+
`waiting for a person to approve it in Juno: ${url}\n` +
|
|
191
|
+
`wait for the outcome with: juno ask --session ${id}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
185
194
|
if (event.pending_input?.kind === 'approval') {
|
|
186
195
|
return (
|
|
187
196
|
`awaiting approval; approve with: juno session approve ${id}\n` +
|
package/src/verbs.js
CHANGED
|
@@ -2,7 +2,9 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
|
|
3
3
|
// createRequire rather than an import attribute: `with { type: 'json' }` needs
|
|
4
4
|
// Node 20.10+, and engines says >=20.
|
|
5
|
-
const { commands: ROWS } = createRequire(import.meta.url)(
|
|
5
|
+
const { commands: ROWS, groups: GROUPS } = createRequire(import.meta.url)(
|
|
6
|
+
'../verbs.json'
|
|
7
|
+
);
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* verbs.json is the command table: every `juno <group> <verb>` the CLI
|
|
@@ -12,6 +14,9 @@ const { commands: ROWS } = createRequire(import.meta.url)('../verbs.json');
|
|
|
12
14
|
*/
|
|
13
15
|
export const rows = ROWS;
|
|
14
16
|
|
|
17
|
+
/** The one line `juno --help` lists each group by, keyed by group. */
|
|
18
|
+
export const groupSummaries = GROUPS;
|
|
19
|
+
|
|
15
20
|
const byCommand = new Map(ROWS.map((row) => [row.command, row]));
|
|
16
21
|
|
|
17
22
|
/** The row for a command string ("state get"), or undefined. */
|