@junoflow/cli 0.1.5 → 0.6.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 +579 -54
- package/bin/juno.js +7 -0
- package/eslint.flow.config.js +33 -0
- package/package.json +7 -4
- package/src/api.js +395 -27
- package/src/authoring-skills.js +42 -0
- package/src/commands/agent-skill-install.js +64 -0
- package/src/commands/ask.js +60 -0
- package/src/commands/check.js +44 -16
- package/src/commands/config.js +26 -0
- package/src/commands/credential-set.js +68 -0
- package/src/commands/deploy.js +41 -50
- package/src/commands/flow-check.js +93 -0
- package/src/commands/flow-deploy.js +107 -0
- package/src/commands/flow-gen-types.js +30 -0
- package/src/commands/flow-init.js +59 -0
- package/src/commands/flow-pull.js +86 -0
- package/src/commands/{types.js → gen-types.js} +12 -5
- package/src/commands/init.js +28 -16
- package/src/commands/pull.js +70 -16
- package/src/commands/session-approve.js +73 -0
- package/src/commands/skill.js +73 -0
- package/src/commands/tool.js +249 -0
- package/src/commands/verify-types.js +4 -2
- package/src/config.js +34 -0
- package/src/diagnostics.js +14 -0
- package/src/errors.js +40 -0
- package/src/exit.js +22 -0
- package/src/flow-project.js +128 -0
- package/src/help.js +199 -0
- package/src/index.js +506 -75
- package/src/installed-skill.js +55 -0
- package/src/prompt.js +196 -0
- package/src/stamp.js +10 -9
- package/src/suggest.js +67 -0
- package/src/target.js +22 -7
- package/src/verbs.js +34 -0
- package/src/watch.js +50 -0
- package/verbs.json +227 -0
package/src/prompt.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { UnreachableError, UserError } from './errors.js';
|
|
2
|
+
import {
|
|
3
|
+
EXIT_AWAITING_INPUT,
|
|
4
|
+
EXIT_FAILURE,
|
|
5
|
+
EXIT_OK,
|
|
6
|
+
EXIT_PENDING,
|
|
7
|
+
} from './exit.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Agent Prompt API client both `juno ask` and `juno session approve` use:
|
|
11
|
+
* the request with its retries, the 409 contract, and the outcome → exit code
|
|
12
|
+
* mapping. Everything here follows docs/…/reference/agent-api.md; the CLI adds
|
|
13
|
+
* no semantics of its own beyond which of the server's answers ends a command.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Transport retries after a failed fetch, each reusing both ids. */
|
|
17
|
+
const TRANSPORT_RETRIES = 2;
|
|
18
|
+
const TRANSPORT_BACKOFF_MS = [500, 1000];
|
|
19
|
+
|
|
20
|
+
/** Rounds of observe-then-resend a busy session gets before giving up. */
|
|
21
|
+
const BUSY_ROUNDS = 3;
|
|
22
|
+
|
|
23
|
+
/** The default `wait_timeout_seconds`, which stays under a harness's Bash timeout. */
|
|
24
|
+
export const DEFAULT_WAIT_SECONDS = 60;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* converse sends `body` (which always names its `session_id`: the CLI mints
|
|
28
|
+
* one for a new conversation) and returns the turn's final event as
|
|
29
|
+
* `{event, text}`, `text` being the bytes it arrived as.
|
|
30
|
+
*
|
|
31
|
+
* The 409 contract, told apart by the body as the API reference documents
|
|
32
|
+
* it:
|
|
33
|
+
* - "session is busy" / "is not idle" / "is not awaiting input": observe
|
|
34
|
+
* until the turn ends, then send again with the same message_id, at most
|
|
35
|
+
* BUSY_ROUNDS times. With `resendWhenBusy: false` (session approve, which
|
|
36
|
+
* must not approve something other than what it checked) it is exit 1.
|
|
37
|
+
* An observe that ends awaiting input is returned instead of resent: the
|
|
38
|
+
* message was not meant as an answer to a question asked after it was
|
|
39
|
+
* typed, and to an approval any answer but `approve` is a denial.
|
|
40
|
+
* - "session was just created": retry at once, once.
|
|
41
|
+
* - "session has ended", and anything else: exit 1 with the server's words.
|
|
42
|
+
*/
|
|
43
|
+
export async function converse(
|
|
44
|
+
client,
|
|
45
|
+
body,
|
|
46
|
+
{ stream, io, resendWhenBusy = true }
|
|
47
|
+
) {
|
|
48
|
+
let retriedJustCreated = false;
|
|
49
|
+
let rounds = 0;
|
|
50
|
+
for (;;) {
|
|
51
|
+
const answer = await exchange(client, body, { stream, io });
|
|
52
|
+
if (!answer.conflict) return answer;
|
|
53
|
+
|
|
54
|
+
const kind = conflictKind(answer.conflict);
|
|
55
|
+
if (kind === 'just-created' && !retriedJustCreated) {
|
|
56
|
+
retriedJustCreated = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (kind !== 'busy' || !resendWhenBusy || rounds >= BUSY_ROUNDS) {
|
|
60
|
+
throw new UserError(answer.conflict);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
rounds += 1;
|
|
64
|
+
io.warn(`${answer.conflict}`);
|
|
65
|
+
io.warn(`waiting for session ${body.session_id}'s current turn to end…`);
|
|
66
|
+
const observed = await exchange(client, observeBody(body), { stream, io });
|
|
67
|
+
if (observed.conflict) throw new UserError(observed.conflict);
|
|
68
|
+
if (observed.event.type === 'awaiting_input') {
|
|
69
|
+
io.warn(
|
|
70
|
+
'your message was not sent: the session is now awaiting input, ' +
|
|
71
|
+
'and it would have been taken as the answer'
|
|
72
|
+
);
|
|
73
|
+
return observed;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* exchange is one request, retried at most TRANSPORT_RETRIES times when
|
|
80
|
+
* there was no answer at all. A retry is safe because it carries the same
|
|
81
|
+
* session_id and message_id: the server either creates the session under
|
|
82
|
+
* that id or, finding the message already delivered, observes its turn.
|
|
83
|
+
*/
|
|
84
|
+
async function exchange(client, body, { stream, io }) {
|
|
85
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
86
|
+
try {
|
|
87
|
+
return await once(client, body, { stream, io });
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (!(err instanceof UnreachableError) || attempt >= TRANSPORT_RETRIES) {
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
io.warn(`${err.message}; retrying`);
|
|
93
|
+
await sleep(TRANSPORT_BACKOFF_MS[attempt]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function once(client, body, { stream, io }) {
|
|
99
|
+
const answer = await client.prompt(body, { stream });
|
|
100
|
+
if (!answer.events) return answer;
|
|
101
|
+
|
|
102
|
+
// Streaming: progress to stderr, one line an event, until the final one.
|
|
103
|
+
for await (const line of answer.events) {
|
|
104
|
+
const { event } = line;
|
|
105
|
+
if (FINAL.has(event.type)) return line;
|
|
106
|
+
const note = progress(event);
|
|
107
|
+
if (note) io.warn(note);
|
|
108
|
+
}
|
|
109
|
+
throw new UnreachableError('the prompt stream ended before the turn did');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The event types that end a turn, and with it the stream. */
|
|
113
|
+
const FINAL = new Set([
|
|
114
|
+
'message',
|
|
115
|
+
'awaiting_input',
|
|
116
|
+
'done',
|
|
117
|
+
'error',
|
|
118
|
+
'pending',
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
function progress(event) {
|
|
122
|
+
switch (event.type) {
|
|
123
|
+
case 'session':
|
|
124
|
+
return `session ${event.session_id}`;
|
|
125
|
+
case 'turn':
|
|
126
|
+
return '· turn finished';
|
|
127
|
+
case 'child':
|
|
128
|
+
return `· delegated to authoring session ${event.child_session_id}`;
|
|
129
|
+
default:
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Observe: the same session and wait, with no message and so no message_id. */
|
|
135
|
+
function observeBody(body) {
|
|
136
|
+
const { message: _m, message_id: _id, ...rest } = body;
|
|
137
|
+
return rest;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function conflictKind(text) {
|
|
141
|
+
if (/^session is busy\b/.test(text)) return 'busy';
|
|
142
|
+
if (/\bis not (idle|awaiting input)\b/.test(text)) return 'busy';
|
|
143
|
+
if (/^session was just created\b/.test(text)) return 'just-created';
|
|
144
|
+
if (/^session has ended\b/.test(text)) return 'ended';
|
|
145
|
+
return 'other';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* outcomeExit maps a turn's final event to the exit code: 0 a reply or a
|
|
150
|
+
* completed session, 5 still running, 6 awaiting input, 1 anything that
|
|
151
|
+
* failed.
|
|
152
|
+
*/
|
|
153
|
+
export function outcomeExit(event) {
|
|
154
|
+
switch (event.type) {
|
|
155
|
+
case 'message':
|
|
156
|
+
return EXIT_OK;
|
|
157
|
+
case 'done':
|
|
158
|
+
return event.status === 'completed' ? EXIT_OK : EXIT_FAILURE;
|
|
159
|
+
case 'pending':
|
|
160
|
+
return EXIT_PENDING;
|
|
161
|
+
case 'awaiting_input':
|
|
162
|
+
return EXIT_AWAITING_INPUT;
|
|
163
|
+
default:
|
|
164
|
+
return EXIT_FAILURE;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* finish prints the final event on stdout — unchanged off a TTY, indented on
|
|
170
|
+
* one — says on stderr how to go on when the turn has not ended the
|
|
171
|
+
* conversation's business, and returns the exit code.
|
|
172
|
+
*/
|
|
173
|
+
export function finish({ event, text }, sessionID, io) {
|
|
174
|
+
io.log(io.isTTY ? JSON.stringify(event, null, 2) : text.trimEnd());
|
|
175
|
+
const next = nextStep(event, sessionID);
|
|
176
|
+
if (next) io.warn(next);
|
|
177
|
+
return outcomeExit(event);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function nextStep(event, id) {
|
|
181
|
+
if (event.type === 'pending') {
|
|
182
|
+
return `still running; keep waiting with: juno ask --session ${id}`;
|
|
183
|
+
}
|
|
184
|
+
if (event.type !== 'awaiting_input') return null;
|
|
185
|
+
if (event.pending_input?.kind === 'approval') {
|
|
186
|
+
return (
|
|
187
|
+
`awaiting approval; approve with: juno session approve ${id}\n` +
|
|
188
|
+
`or deny, with a reason the assistant sees: juno ask --session ${id} "<reason>"`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return `awaiting an answer; reply with: juno ask --session ${id} "<answer>"`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function sleep(ms) {
|
|
195
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
|
+
}
|
package/src/stamp.js
CHANGED
|
@@ -7,16 +7,17 @@ import { createHash } from 'node:crypto';
|
|
|
7
7
|
* and TestManifestStampParity pin the same input to the same output from each
|
|
8
8
|
* side, which is the only thing that keeps them together.
|
|
9
9
|
*
|
|
10
|
-
* Why it exists: "edit manifest.json, forget `juno types`" is
|
|
11
|
-
* this workflow reliably produces, and nothing else catches
|
|
12
|
-
* happily against stale types, eslint is happy, and
|
|
13
|
-
* manifest against the *module*, never against
|
|
14
|
-
* deploy, or later still, as a flow author
|
|
15
|
-
* no longer exists.
|
|
10
|
+
* Why it exists: "edit manifest.json, forget `juno integration gen-types`" is
|
|
11
|
+
* the one mistake this workflow reliably produces, and nothing else catches
|
|
12
|
+
* it. tsc compiles happily against stale types, eslint is happy, and
|
|
13
|
+
* h.conformance() checks the manifest against the *module*, never against
|
|
14
|
+
* types/. The drift surfaces at deploy, or later still, as a flow author
|
|
15
|
+
* calling an action with a shape that no longer exists.
|
|
16
16
|
*
|
|
17
17
|
* Why a hash rather than asking a Juno: the check has to run where the mistake
|
|
18
18
|
* is caught — a contributor's CI, which deliberately has no instance to ask.
|
|
19
|
-
* `juno types` needs a server; verifying what it wrote must
|
|
19
|
+
* `juno integration gen-types` needs a server; verifying what it wrote must
|
|
20
|
+
* not.
|
|
20
21
|
*/
|
|
21
22
|
const STAMP_PREFIX = '// juno:manifest-sha256 ';
|
|
22
23
|
const STAMP_LENGTH = 16;
|
|
@@ -68,8 +69,8 @@ export function stampEnvDTS(envDTS, manifestJSON) {
|
|
|
68
69
|
* Applied client-side rather than server-side because it fingerprints the bytes
|
|
69
70
|
* on *this* disk: both endpoints are handed a parsed manifest and never see the
|
|
70
71
|
* file, and the same typegen endpoint also answers the authoring agent, which
|
|
71
|
-
* has no file at all. The project generator behind `juno init`
|
|
72
|
-
* it stamps there — internal/typegen/stamp.go.
|
|
72
|
+
* has no file at all. The project generator behind `juno integration init`
|
|
73
|
+
* does see one, so it stamps there — internal/typegen/stamp.go.
|
|
73
74
|
*/
|
|
74
75
|
export function stampTypeFiles(types, rawManifest) {
|
|
75
76
|
const name = ENV_DTS.replace('types/', '');
|
package/src/suggest.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Suggestions further than this are more likely noise than a typo. */
|
|
2
|
+
const MAX_DISTANCE = 3;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* closest is the candidate nearest to any of `attempts` by edit distance, or
|
|
6
|
+
* null when none is within MAX_DISTANCE. Ties go to the earlier candidate, so
|
|
7
|
+
* the order of verbs.json decides.
|
|
8
|
+
*/
|
|
9
|
+
export function closest(attempts, candidates) {
|
|
10
|
+
let best = null;
|
|
11
|
+
let bestDistance = MAX_DISTANCE + 1;
|
|
12
|
+
for (const candidate of candidates) {
|
|
13
|
+
for (const attempt of attempts) {
|
|
14
|
+
const d = levenshtein(attempt, candidate);
|
|
15
|
+
if (d < bestDistance) {
|
|
16
|
+
best = candidate;
|
|
17
|
+
bestDistance = d;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return best;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** levenshtein is the plain edit distance: insert, delete, substitute. */
|
|
25
|
+
export function levenshtein(a, b) {
|
|
26
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
27
|
+
for (let i = 1; i <= a.length; i++) {
|
|
28
|
+
const cur = [i];
|
|
29
|
+
for (let j = 1; j <= b.length; j++) {
|
|
30
|
+
cur[j] = Math.min(
|
|
31
|
+
prev[j] + 1,
|
|
32
|
+
cur[j - 1] + 1,
|
|
33
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
prev = cur;
|
|
37
|
+
}
|
|
38
|
+
return prev[b.length];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* closestByVerb is for a bare word that is not a command: the grouped
|
|
43
|
+
* commands whose verb is nearest to it, all of them when several groups share
|
|
44
|
+
* that verb, with the distance. A bare `deploy` is the verb of both
|
|
45
|
+
* `flow deploy` and `integration deploy`, and only the person knows which
|
|
46
|
+
* working copy they are in, so both are offered rather than whichever
|
|
47
|
+
* verbs.json lists first. A verb one of whose `-` words is the attempt counts
|
|
48
|
+
* as distance 1, so a bare `types` still finds `gen-types`.
|
|
49
|
+
*/
|
|
50
|
+
export function closestByVerb(attempt, commands) {
|
|
51
|
+
let best = [];
|
|
52
|
+
let bestDistance = MAX_DISTANCE + 1;
|
|
53
|
+
for (const command of commands) {
|
|
54
|
+
const space = command.indexOf(' ');
|
|
55
|
+
if (space < 0) continue;
|
|
56
|
+
const verb = command.slice(space + 1);
|
|
57
|
+
let d = levenshtein(attempt, verb);
|
|
58
|
+
if (d > 1 && verb.split('-').includes(attempt)) d = 1;
|
|
59
|
+
if (d < bestDistance) {
|
|
60
|
+
best = [command];
|
|
61
|
+
bestDistance = d;
|
|
62
|
+
} else if (d === bestDistance) {
|
|
63
|
+
best.push(command);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { commands: best, distance: bestDistance };
|
|
67
|
+
}
|
package/src/target.js
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { readUserConfig } from './config.js';
|
|
4
5
|
import { UserError } from './errors.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Resolve which Juno instance to talk to, in precedence order: an explicit
|
|
8
9
|
* --url, then $JUNO_URL, then `juno.target` in package.json — which is what an
|
|
9
|
-
* ejected project arrives with, so a fresh clone deploys with no setup
|
|
10
|
+
* ejected project arrives with, so a fresh clone deploys with no setup — then
|
|
11
|
+
* the user config file `juno config set url` writes, for use outside a
|
|
12
|
+
* project.
|
|
10
13
|
*
|
|
11
|
-
* A missing target names all
|
|
12
|
-
* guessing which of
|
|
14
|
+
* A missing target names all four: "no target configured" leaves the reader
|
|
15
|
+
* guessing which of them they were supposed to have used.
|
|
13
16
|
*/
|
|
14
|
-
export async function resolveTarget({
|
|
17
|
+
export async function resolveTarget({
|
|
18
|
+
url,
|
|
19
|
+
cwd = process.cwd(),
|
|
20
|
+
env = process.env,
|
|
21
|
+
} = {}) {
|
|
15
22
|
const candidate =
|
|
16
|
-
url ??
|
|
23
|
+
url ??
|
|
24
|
+
env.JUNO_URL ??
|
|
25
|
+
(await targetFromPackageJSON(cwd)) ??
|
|
26
|
+
(await targetFromUserConfig(env));
|
|
17
27
|
if (!candidate) {
|
|
18
28
|
throw new UserError(
|
|
19
|
-
'no Juno instance
|
|
29
|
+
'no Juno instance configured. Pass --url <url>, set $JUNO_URL, add\n' +
|
|
20
30
|
' "juno": { "target": "http://juno.local:8080" }\n' +
|
|
21
|
-
'to package.json
|
|
31
|
+
'to package.json, or run `juno config set url <url>`.'
|
|
22
32
|
);
|
|
23
33
|
}
|
|
24
34
|
return normalizeTarget(candidate);
|
|
@@ -33,6 +43,11 @@ async function targetFromPackageJSON(cwd) {
|
|
|
33
43
|
}
|
|
34
44
|
}
|
|
35
45
|
|
|
46
|
+
async function targetFromUserConfig(env) {
|
|
47
|
+
const { url } = await readUserConfig(env);
|
|
48
|
+
return typeof url === 'string' && url.trim() ? url : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
/** Accepts a bare host the way a browser address bar does. */
|
|
37
52
|
export function normalizeTarget(target) {
|
|
38
53
|
let t = target.trim().replace(/\/+$/, '');
|
package/src/verbs.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
// createRequire rather than an import attribute: `with { type: 'json' }` needs
|
|
4
|
+
// Node 20.10+, and engines says >=20.
|
|
5
|
+
const { commands: ROWS } = createRequire(import.meta.url)('../verbs.json');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* verbs.json is the command table: every `juno <group> <verb>` the CLI
|
|
9
|
+
* answers, and the one place a command name is decided. The Go tests in
|
|
10
|
+
* internal/agent/verbs_test.go hold it to the tool gateway; this module only
|
|
11
|
+
* indexes it.
|
|
12
|
+
*/
|
|
13
|
+
export const rows = ROWS;
|
|
14
|
+
|
|
15
|
+
const byCommand = new Map(ROWS.map((row) => [row.command, row]));
|
|
16
|
+
|
|
17
|
+
/** The row for a command string ("state get"), or undefined. */
|
|
18
|
+
export function rowFor(command) {
|
|
19
|
+
return byCommand.get(command);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A verb spelled with `_` is an alias for the `-` spelling, so an agent that
|
|
24
|
+
* types the tool's own word order (`juno state get_history`) still lands.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeVerb(verb) {
|
|
27
|
+
return verb.replaceAll('_', '-');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The group of a two-word command, or null for a top-level one. */
|
|
31
|
+
export function groupOf(row) {
|
|
32
|
+
const space = row.command.indexOf(' ');
|
|
33
|
+
return space < 0 ? null : row.command.slice(0, space);
|
|
34
|
+
}
|
package/src/watch.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { DiagnosticsError } from './errors.js';
|
|
4
|
+
import { formatDiagnostics } from './diagnostics.js';
|
|
5
|
+
|
|
6
|
+
/** How long to wait for edits to settle before redeploying. */
|
|
7
|
+
export const WATCH_DEBOUNCE_MS = 200;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* deployOnChange is `deploy --watch` for both kinds of working copy: run
|
|
11
|
+
* `once`, then again whenever one of `paths` changes (`{path, recursive?}`),
|
|
12
|
+
* debounced so an editor's save-and-rename is one deploy. A failed deploy is
|
|
13
|
+
* printed and the loop keeps watching — the next save is the retry. It
|
|
14
|
+
* resolves only on SIGINT: watch mode ends when the author ends it.
|
|
15
|
+
*/
|
|
16
|
+
export async function deployOnChange({ io, target, paths, once }) {
|
|
17
|
+
io.log(`watching ${io.cwd} — deploying to ${target}`);
|
|
18
|
+
const iteration = async () => {
|
|
19
|
+
try {
|
|
20
|
+
await once();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err instanceof DiagnosticsError) {
|
|
23
|
+
io.error(formatDiagnostics(err.diagnostics));
|
|
24
|
+
} else {
|
|
25
|
+
io.error(err.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
await iteration();
|
|
30
|
+
|
|
31
|
+
let timer = null;
|
|
32
|
+
const schedule = () => {
|
|
33
|
+
if (timer) clearTimeout(timer);
|
|
34
|
+
timer = setTimeout(() => {
|
|
35
|
+
timer = null;
|
|
36
|
+
void iteration();
|
|
37
|
+
}, WATCH_DEBOUNCE_MS);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const watchers = paths.map(({ path, recursive = false }) =>
|
|
41
|
+
watch(path, { recursive }, schedule)
|
|
42
|
+
);
|
|
43
|
+
await new Promise((resolve) => {
|
|
44
|
+
process.once('SIGINT', () => {
|
|
45
|
+
for (const w of watchers) w.close();
|
|
46
|
+
resolve();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
package/verbs.json
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
{
|
|
2
|
+
"commands": [
|
|
3
|
+
{
|
|
4
|
+
"command": "discover",
|
|
5
|
+
"tool": "discover",
|
|
6
|
+
"id_field": "query",
|
|
7
|
+
"flags": {
|
|
8
|
+
"kind": { "field": "kinds", "type": "string", "multiple": true }
|
|
9
|
+
},
|
|
10
|
+
"naming_exception": "searches every kind, so it has no group"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"command": "related",
|
|
14
|
+
"tool": "get_related",
|
|
15
|
+
"positional": "kind:id",
|
|
16
|
+
"flags": { "degree": { "field": "degree", "type": "integer" } },
|
|
17
|
+
"naming_exception": "takes any kind, so it has no group"
|
|
18
|
+
},
|
|
19
|
+
{ "command": "state get", "tool": "get_state", "id_field": "id" },
|
|
20
|
+
{
|
|
21
|
+
"command": "state get-history",
|
|
22
|
+
"tool": "get_state_history",
|
|
23
|
+
"id_field": "id"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"command": "state get-layers",
|
|
27
|
+
"tool": "get_state_layers",
|
|
28
|
+
"id_field": "id"
|
|
29
|
+
},
|
|
30
|
+
{ "command": "state evaluate-level", "tool": "evaluate_level" },
|
|
31
|
+
{
|
|
32
|
+
"command": "state preview-layers",
|
|
33
|
+
"tool": "edit_state_layers",
|
|
34
|
+
"id_field": "id",
|
|
35
|
+
"fixed_input": { "dry_run": true },
|
|
36
|
+
"effect": "read",
|
|
37
|
+
"naming_exception": "fixes dry_run: true on edit_state_layers, so a permission rule can allow the preview and not the edit"
|
|
38
|
+
},
|
|
39
|
+
{ "command": "state create", "tool": "create_state", "id_field": "id" },
|
|
40
|
+
{
|
|
41
|
+
"command": "state edit-layers",
|
|
42
|
+
"tool": "edit_state_layers",
|
|
43
|
+
"id_field": "id",
|
|
44
|
+
"fixed_input": { "dry_run": false }
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"command": "state set-value",
|
|
48
|
+
"tool": "set_state_value",
|
|
49
|
+
"id_field": "id"
|
|
50
|
+
},
|
|
51
|
+
{ "command": "state claim", "tool": "claim_state", "id_field": "id" },
|
|
52
|
+
{ "command": "state release", "tool": "release_state", "id_field": "id" },
|
|
53
|
+
{ "command": "flow get", "tool": "get_flow", "id_field": "id" },
|
|
54
|
+
{
|
|
55
|
+
"command": "flow set-enabled",
|
|
56
|
+
"tool": "set_flow_enabled",
|
|
57
|
+
"id_field": "flow_id"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"command": "flow trigger",
|
|
61
|
+
"tool": "trigger_flow",
|
|
62
|
+
"id_field": "flow_id"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"command": "flow fire-trigger",
|
|
66
|
+
"tool": "fire_trigger",
|
|
67
|
+
"id_field": "flow_id"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"command": "flow init",
|
|
71
|
+
"effect": "read",
|
|
72
|
+
"working_copy": true,
|
|
73
|
+
"summary": "Write a working copy of a flow (or a stub for a new id) into ./<id>"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"command": "flow gen-types",
|
|
77
|
+
"effect": "read",
|
|
78
|
+
"working_copy": true,
|
|
79
|
+
"summary": "Regenerate flow-env.d.ts and flow-api.d.ts from manifest.json"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"command": "flow check",
|
|
83
|
+
"effect": "read",
|
|
84
|
+
"working_copy": true,
|
|
85
|
+
"summary": "Typecheck and lint locally, then verify against Juno without deploying"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"command": "flow deploy",
|
|
89
|
+
"effect": "change",
|
|
90
|
+
"working_copy": true,
|
|
91
|
+
"summary": "Verify, commit and activate the working copy as the flow's next version"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"command": "flow pull",
|
|
95
|
+
"effect": "read",
|
|
96
|
+
"working_copy": true,
|
|
97
|
+
"summary": "Overwrite the working copy with the flow's active version"
|
|
98
|
+
},
|
|
99
|
+
{ "command": "run list", "tool": "list_runs" },
|
|
100
|
+
{ "command": "run get", "tool": "get_run", "id_field": "run_id" },
|
|
101
|
+
{
|
|
102
|
+
"command": "integration get",
|
|
103
|
+
"tool": "get_integration",
|
|
104
|
+
"id_field": "id"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"command": "integration configure",
|
|
108
|
+
"tool": "configure_integration",
|
|
109
|
+
"id_field": "id"
|
|
110
|
+
},
|
|
111
|
+
{ "command": "integration install", "tool": "install_integration" },
|
|
112
|
+
{
|
|
113
|
+
"command": "integration run-schedule",
|
|
114
|
+
"tool": "run_integration_schedule",
|
|
115
|
+
"id_field": "integration"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"command": "integration init",
|
|
119
|
+
"effect": "read",
|
|
120
|
+
"working_copy": true,
|
|
121
|
+
"summary": "Write a working copy of an install (or a starter for a new id) into ./<id>"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"command": "integration gen-types",
|
|
125
|
+
"effect": "read",
|
|
126
|
+
"working_copy": true,
|
|
127
|
+
"summary": "Regenerate types/ from manifest.json"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"command": "integration verify-types",
|
|
131
|
+
"effect": "read",
|
|
132
|
+
"working_copy": true,
|
|
133
|
+
"summary": "Check offline that types/ matches manifest.json"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"command": "integration check",
|
|
137
|
+
"effect": "read",
|
|
138
|
+
"working_copy": true,
|
|
139
|
+
"summary": "Typecheck and lint locally, then verify against Juno without deploying"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"command": "integration deploy",
|
|
143
|
+
"effect": "change",
|
|
144
|
+
"working_copy": true,
|
|
145
|
+
"summary": "Verify, commit and activate the working copy as the install's next version"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"command": "integration pull",
|
|
149
|
+
"effect": "read",
|
|
150
|
+
"working_copy": true,
|
|
151
|
+
"summary": "Overwrite manifest.json, src/ and package.json scripts with the active version"
|
|
152
|
+
},
|
|
153
|
+
{ "command": "device list", "tool": "list_devices" },
|
|
154
|
+
{ "command": "device get", "tool": "get_device", "id_field": "id" },
|
|
155
|
+
{ "command": "device preview", "tool": "preview_device" },
|
|
156
|
+
{ "command": "device write", "tool": "write_device" },
|
|
157
|
+
{ "command": "device suspend", "tool": "suspend_device", "id_field": "id" },
|
|
158
|
+
{ "command": "device release", "tool": "release_device", "id_field": "id" },
|
|
159
|
+
{ "command": "control write", "tool": "write_control" },
|
|
160
|
+
{ "command": "control use", "tool": "use_control", "id_field": "id" },
|
|
161
|
+
{
|
|
162
|
+
"command": "control release",
|
|
163
|
+
"tool": "release_control",
|
|
164
|
+
"id_field": "id"
|
|
165
|
+
},
|
|
166
|
+
{ "command": "finding get", "tool": "get_finding", "id_field": "slug" },
|
|
167
|
+
{ "command": "webhook list", "tool": "list_webhooks" },
|
|
168
|
+
{
|
|
169
|
+
"command": "webhook get-deliveries",
|
|
170
|
+
"tool": "get_webhook_deliveries",
|
|
171
|
+
"id_field": "id"
|
|
172
|
+
},
|
|
173
|
+
{ "command": "webhook write", "tool": "write_webhook", "id_field": "id" },
|
|
174
|
+
{ "command": "webhook fire", "tool": "fire_webhook", "id_field": "id" },
|
|
175
|
+
{ "command": "credential list", "tool": "list_credentials" },
|
|
176
|
+
{
|
|
177
|
+
"command": "ha get-entity",
|
|
178
|
+
"tool": "get_entity",
|
|
179
|
+
"id_field": "entity_id"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"command": "ha get-entity-history",
|
|
183
|
+
"tool": "get_entity_history",
|
|
184
|
+
"id_field": "entity_id"
|
|
185
|
+
},
|
|
186
|
+
{ "command": "ha list-services", "tool": "list_services" },
|
|
187
|
+
{ "command": "ha call-service", "tool": "call_service" },
|
|
188
|
+
{
|
|
189
|
+
"command": "script run",
|
|
190
|
+
"tool": "run_script",
|
|
191
|
+
"positional": "file",
|
|
192
|
+
"flags": {
|
|
193
|
+
"cred": { "field": "credentials", "type": "string", "multiple": true }
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"command": "skill",
|
|
198
|
+
"effect": "read",
|
|
199
|
+
"summary": "Print the skills Juno's assistant can load, or one skill's body"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"command": "ask",
|
|
203
|
+
"effect": "act",
|
|
204
|
+
"summary": "Prompt Juno's in-app assistant"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"command": "session approve",
|
|
208
|
+
"effect": "act",
|
|
209
|
+
"summary": "Approve the tool call an assistant session is waiting on"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"command": "credential set",
|
|
213
|
+
"effect": "act",
|
|
214
|
+
"summary": "Set a credential's secret, typed at a terminal with echo off"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"command": "config set",
|
|
218
|
+
"effect": "local",
|
|
219
|
+
"summary": "Remember the instance URL in the user config file"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"command": "agent-skill install",
|
|
223
|
+
"effect": "local",
|
|
224
|
+
"summary": "Write Juno's skill for a coding agent and print its permission block"
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
}
|