@junoflow/cli 0.3.0 → 0.9.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 +693 -52
- package/app-client.js +306 -0
- package/bin/juno.js +7 -0
- package/eslint.app.config.js +34 -0
- package/eslint.flow.config.js +35 -0
- package/package.json +12 -4
- package/src/api.js +541 -30
- package/src/app-dev/build.js +134 -0
- package/src/app-dev/harness.js +108 -0
- package/src/app-dev/panel.html +336 -0
- package/src/app-dev/panel.js +290 -0
- package/src/app-dev/server.js +347 -0
- package/src/app-dev/stream.js +224 -0
- package/src/app-dev/watch.js +47 -0
- package/src/app-project.js +133 -0
- package/src/authoring-skills.js +43 -0
- package/src/commands/agent-skill-install.js +64 -0
- package/src/commands/app-check.js +75 -0
- package/src/commands/app-deploy.js +102 -0
- package/src/commands/app-dev.js +141 -0
- package/src/commands/app-gen-types.js +37 -0
- package/src/commands/app-init.js +46 -0
- package/src/commands/app-pull.js +106 -0
- package/src/commands/ask.js +60 -0
- package/src/commands/check.js +71 -27
- package/src/commands/config.js +26 -0
- package/src/commands/credential-set.js +68 -0
- package/src/commands/deploy.js +95 -51
- package/src/commands/flow-check.js +75 -0
- package/src/commands/flow-deploy.js +110 -0
- package/src/commands/flow-fixture.js +67 -0
- package/src/commands/flow-gen-types.js +37 -0
- package/src/commands/flow-init.js +59 -0
- package/src/commands/flow-pull.js +115 -0
- package/src/commands/{types.js → gen-types.js} +12 -5
- package/src/commands/init.js +28 -16
- package/src/commands/pull.js +111 -63
- 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 +78 -0
- package/src/help.js +199 -0
- package/src/index.js +567 -75
- package/src/installed-skill.js +55 -0
- package/src/project.js +76 -8
- 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 +87 -0
- package/src/working-copy.js +262 -0
- package/tsconfig.page.json +15 -0
- package/verbs.json +276 -0
|
@@ -5,8 +5,10 @@ import { UserError } from '../errors.js';
|
|
|
5
5
|
import { readManifestRaw } from '../project.js';
|
|
6
6
|
import { ENV_DTS, manifestStamp, readStamp } from '../stamp.js';
|
|
7
7
|
|
|
8
|
+
export const VERIFY_TYPES_USAGE = 'juno integration verify-types';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
|
-
* `juno verify-types` — does types/ still describe manifest.json?
|
|
11
|
+
* `juno integration verify-types` — does types/ still describe manifest.json?
|
|
10
12
|
*
|
|
11
13
|
* The one check in this CLI that talks to nothing. It is the counterweight to
|
|
12
14
|
* committing types/: that decision is what lets a drive-by contributor work
|
|
@@ -16,7 +18,7 @@ import { ENV_DTS, manifestStamp, readStamp } from '../stamp.js';
|
|
|
16
18
|
* rather than noticing they are stale, and h.conformance() compares the
|
|
17
19
|
* manifest with the module, never with types/.
|
|
18
20
|
*
|
|
19
|
-
* So this runs first in `npm test` and inside `juno check`. A failure here is
|
|
21
|
+
* So this runs first in `npm test` and inside `juno integration check`. A failure here is
|
|
20
22
|
* always the same one sentence of advice, which is why the message says it
|
|
21
23
|
* rather than describing the mismatch.
|
|
22
24
|
*/
|
package/src/config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The user config file: `$XDG_CONFIG_HOME/juno/config.json`, or
|
|
7
|
+
* `~/.config/juno/config.json` when XDG_CONFIG_HOME is unset. It holds
|
|
8
|
+
* `{"url": "…"}`, the last place resolveTarget looks, and is what makes `juno`
|
|
9
|
+
* usable outside a project after a global install.
|
|
10
|
+
*/
|
|
11
|
+
export function configPath(env = process.env) {
|
|
12
|
+
const base = env.XDG_CONFIG_HOME || join(env.HOME || homedir(), '.config');
|
|
13
|
+
return join(base, 'juno', 'config.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The parsed config; a missing or unreadable file is `{}`. */
|
|
17
|
+
export async function readUserConfig(env = process.env) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(await readFile(configPath(env), 'utf8'));
|
|
20
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
21
|
+
? parsed
|
|
22
|
+
: {};
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Writes the whole config, creating its directory. */
|
|
29
|
+
export async function writeUserConfig(env = process.env, cfg) {
|
|
30
|
+
const path = configPath(env);
|
|
31
|
+
await mkdir(dirname(path), { recursive: true });
|
|
32
|
+
await writeFile(path, `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
|
|
33
|
+
return path;
|
|
34
|
+
}
|
package/src/diagnostics.js
CHANGED
|
@@ -14,3 +14,17 @@ export function formatDiagnostic(d) {
|
|
|
14
14
|
export function formatDiagnostics(diagnostics) {
|
|
15
15
|
return diagnostics.map(formatDiagnostic).join('\n');
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* --json on check and deploy (both kinds): the route's body on stdout,
|
|
20
|
+
* indented only on a TTY, for CI.
|
|
21
|
+
*/
|
|
22
|
+
export function printJSON(body, io) {
|
|
23
|
+
io.log(io.isTTY ? JSON.stringify(body, null, 2) : JSON.stringify(body));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** --json on a failed verify: the 422's detail and diagnostics, exit 1. */
|
|
27
|
+
export function printDiagnosticsJSON(err, io) {
|
|
28
|
+
printJSON({ detail: err.message, diagnostics: err.diagnostics }, io);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
package/src/errors.js
CHANGED
|
@@ -24,3 +24,43 @@ export class DiagnosticsError extends Error {
|
|
|
24
24
|
this.diagnostics = diagnostics ?? [];
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* UsageError is a command line the CLI cannot act on: an unknown command or
|
|
30
|
+
* flag, a missing argument, a malformed --input. The top-level handler prints
|
|
31
|
+
* its message and exits 2. The fix is to change the command line — the message
|
|
32
|
+
* should say how, rather than restating the whole help.
|
|
33
|
+
*/
|
|
34
|
+
export class UsageError extends Error {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = 'UsageError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* UnreachableError means the request never got an answer: the fetch itself
|
|
43
|
+
* failed (connection refused, DNS, TLS). The top-level handler exits 3. The
|
|
44
|
+
* fix is on the network side — is the instance running, and is --url (or
|
|
45
|
+
* $JUNO_URL, package.json, the user config) pointing at it?
|
|
46
|
+
*/
|
|
47
|
+
export class UnreachableError extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'UnreachableError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* ConflictError is a deploy or check the server refused because the working
|
|
56
|
+
* copy is stale: the flow's, integration's or app's active version is not the
|
|
57
|
+
* one `juno.base_version` says it was pulled from (a 409). The top-level handler prints the server's message,
|
|
58
|
+
* which names both versions, and exits 4. The fix is `npm run pull`, or
|
|
59
|
+
* deploying with --force to replace whatever is active.
|
|
60
|
+
*/
|
|
61
|
+
export class ConflictError extends Error {
|
|
62
|
+
constructor(message) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = 'ConflictError';
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/exit.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit codes, one table for the whole CLI (plan-external-agents-cli.md,
|
|
3
|
+
* Decision 11):
|
|
4
|
+
*
|
|
5
|
+
* 0 success
|
|
6
|
+
* 1 a tool result with a top-level string `error`; any other failure
|
|
7
|
+
* (a UserError, diagnostics, a 5xx)
|
|
8
|
+
* 2 usage: unknown command or flag, bad --input, a gateway 422 or 404,
|
|
9
|
+
* a bad --jq
|
|
10
|
+
* 3 the instance is unreachable (the fetch itself failed)
|
|
11
|
+
* 4 a deploy conflict: a working-copy deploy or check (any kind) got a
|
|
12
|
+
* 409 because the active version is not the copy's base_version
|
|
13
|
+
* 5 `juno ask` / `session approve`: the turn is still running (pending)
|
|
14
|
+
* 6 `juno ask` / `session approve`: the assistant is awaiting input
|
|
15
|
+
*/
|
|
16
|
+
export const EXIT_OK = 0;
|
|
17
|
+
export const EXIT_FAILURE = 1;
|
|
18
|
+
export const EXIT_USAGE = 2;
|
|
19
|
+
export const EXIT_UNREACHABLE = 3;
|
|
20
|
+
export const EXIT_CONFLICT = 4;
|
|
21
|
+
export const EXIT_PENDING = 5;
|
|
22
|
+
export const EXIT_AWAITING_INPUT = 6;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { UserError } from './errors.js';
|
|
2
|
+
import { readSourceFiles, readTestFiles } from './project.js';
|
|
3
|
+
import {
|
|
4
|
+
baseVersionOf,
|
|
5
|
+
MANIFEST,
|
|
6
|
+
manifestText,
|
|
7
|
+
PACKAGE_JSON,
|
|
8
|
+
readBoundPackage,
|
|
9
|
+
readManifestFile,
|
|
10
|
+
typeFiles,
|
|
11
|
+
writeChangedFiles,
|
|
12
|
+
writePackageJSON,
|
|
13
|
+
} from './working-copy.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A flow working copy is laid out as an integration's is: manifest.json, the
|
|
17
|
+
* sources under src/ (entry src/flow.ts) and the tests under test/ are the
|
|
18
|
+
* author's, types/ is
|
|
19
|
+
* generated, and package.json's `juno` block binds the directory to one flow.
|
|
20
|
+
* The layout is the server's (POST /api/flows/scaffold); this module only
|
|
21
|
+
* knows the handful of names a command reads or writes back, and what it
|
|
22
|
+
* shares with an app working copy lives in working-copy.js.
|
|
23
|
+
*/
|
|
24
|
+
export { MANIFEST, PACKAGE_JSON, manifestText, typeFiles };
|
|
25
|
+
export const SRC_DIR = 'src';
|
|
26
|
+
export const FLOW_ENTRY = 'src/flow.ts';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* readFlowProject reads what ingest takes: the manifest (parsed, so a broken
|
|
30
|
+
* one fails here with its parse error rather than as a server 422), every
|
|
31
|
+
* .ts and .json file under src/ and test/, and the binding in package.json.
|
|
32
|
+
* Juno stores test/ with the version and never compiles it. A copy
|
|
33
|
+
* with no src/flow.ts is refused before anything is sent. `flowId` is
|
|
34
|
+
* `juno.flow_id` — the id is identity, so the CLI sends it and the server
|
|
35
|
+
* refuses a manifest naming another — and `baseVersion` is
|
|
36
|
+
* `juno.base_version`, absent for a flow that has never been deployed from
|
|
37
|
+
* this copy.
|
|
38
|
+
*/
|
|
39
|
+
export async function readFlowProject(cwd) {
|
|
40
|
+
const pkg = await readFlowPackage(cwd);
|
|
41
|
+
const files = await readSourceFiles(cwd);
|
|
42
|
+
if (!Object.hasOwn(files, FLOW_ENTRY)) {
|
|
43
|
+
throw new UserError(
|
|
44
|
+
`no ${FLOW_ENTRY} — a flow working copy's entry is ${FLOW_ENTRY}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...(await readFlowManifest(cwd)),
|
|
49
|
+
files: { ...files, ...(await readTestFiles(cwd)) },
|
|
50
|
+
packageJSON: pkg,
|
|
51
|
+
flowId: pkg.juno.flow_id,
|
|
52
|
+
baseVersion: baseVersionOf(pkg),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** readFlowManifest is manifest.json, as bytes and parsed. */
|
|
57
|
+
export const readFlowManifest = readManifestFile;
|
|
58
|
+
|
|
59
|
+
/** readFlowPackage is package.json, refusing a directory that is not a flow working copy. */
|
|
60
|
+
export function readFlowPackage(cwd) {
|
|
61
|
+
return readBoundPackage(cwd, { kind: 'flow', idKey: 'flow_id' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* updatePackageJSON rewrites a flow working copy's package.json through
|
|
66
|
+
* `mutate`, keeping every key it does not touch (and their order).
|
|
67
|
+
*/
|
|
68
|
+
export async function updatePackageJSON(cwd, mutate) {
|
|
69
|
+
const pkg = await readFlowPackage(cwd);
|
|
70
|
+
mutate(pkg);
|
|
71
|
+
await writePackageJSON(cwd, pkg);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* writeFlowFiles writes files by working-copy name, skipping one whose
|
|
76
|
+
* content is already what is on disk. Returns the names it wrote.
|
|
77
|
+
*/
|
|
78
|
+
export const writeFlowFiles = writeChangedFiles;
|
package/src/help.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { connect } from './api.js';
|
|
2
|
+
import { AUTHORING_HEADINGS, authoringHeading } from './authoring-skills.js';
|
|
3
|
+
import { UsageError } from './errors.js';
|
|
4
|
+
import { groupOf } from './verbs.js';
|
|
5
|
+
|
|
6
|
+
const INPUT_USAGE = '[--input <json>|-|@file] [--jq <expr>]';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* rowUsage is one command's synopsis: the positional and flags come from its
|
|
10
|
+
* verbs.json row, a non-tool command's from its handler.
|
|
11
|
+
*/
|
|
12
|
+
export function rowUsage(row, handler) {
|
|
13
|
+
if (!row.tool) return handler.usage;
|
|
14
|
+
const parts = [`juno ${row.command}`];
|
|
15
|
+
const positional = positionalName(row);
|
|
16
|
+
if (positional) parts.push(`[${positional}]`);
|
|
17
|
+
for (const [flag, spec] of Object.entries(row.flags ?? {})) {
|
|
18
|
+
parts.push(`[--${flag} <${spec.type}>${spec.multiple ? '...' : ''}]`);
|
|
19
|
+
}
|
|
20
|
+
parts.push(INPUT_USAGE);
|
|
21
|
+
return parts.join(' ');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function positionalName(row) {
|
|
25
|
+
if (row.id_field) return `<${row.id_field}>`;
|
|
26
|
+
if (row.positional === 'kind:id') return '<kind>:<id> | <kind> <id>';
|
|
27
|
+
if (row.positional === 'file') return '<file>';
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* groupHelp lists a group's commands. The effect and first sentence of each
|
|
33
|
+
* tool's description come from GET /api/tools, because the server owns that
|
|
34
|
+
* text; when the instance cannot answer, the commands are listed alone and
|
|
35
|
+
* the reason goes to stderr. A help must not fail offline.
|
|
36
|
+
*
|
|
37
|
+
* A group with working-copy commands (`juno flow init`, `deploy`, …) lists
|
|
38
|
+
* them under a second heading, from their own `summary`, offline: they act on
|
|
39
|
+
* a directory, not on the instance, and an agent reading the help must be
|
|
40
|
+
* able to tell the two apart. `handlerFor(row)` is a non-tool row's handler.
|
|
41
|
+
*/
|
|
42
|
+
export async function groupHelp(group, rows, handlerFor, args, io) {
|
|
43
|
+
const members = rows.filter((row) => groupOf(row) === group);
|
|
44
|
+
const onInstance = members.filter((row) => !row.working_copy);
|
|
45
|
+
const inWorkingCopy = members.filter((row) => row.working_copy);
|
|
46
|
+
|
|
47
|
+
// A kind with working copies also lists its authoring skills, by their
|
|
48
|
+
// first sentence, under the working-copy heading — the same index `init`
|
|
49
|
+
// prints, fetched the same way and dropped the same way offline.
|
|
50
|
+
const authoring = Object.hasOwn(AUTHORING_HEADINGS, group) ? group : null;
|
|
51
|
+
let tools = null;
|
|
52
|
+
let skills = null;
|
|
53
|
+
if (onInstance.some((row) => row.tool) || authoring) {
|
|
54
|
+
try {
|
|
55
|
+
const { client } = await connect(args, io);
|
|
56
|
+
if (onInstance.some((row) => row.tool)) {
|
|
57
|
+
tools = new Map((await client.listTools()).map((t) => [t.name, t]));
|
|
58
|
+
}
|
|
59
|
+
if (authoring) {
|
|
60
|
+
skills = (await client.listSkills({ authoring }))?.skills ?? [];
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
io.warn(`(descriptions unavailable: ${err.message})`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lines = members.map((row) => {
|
|
68
|
+
const synopsis = row.tool
|
|
69
|
+
? `juno ${row.command}${positionalName(row) ? ` ${positionalName(row)}` : ''}`
|
|
70
|
+
: handlerFor(row).usage;
|
|
71
|
+
if (!row.tool) return [synopsis, row.effect, row.summary];
|
|
72
|
+
const tool = tools?.get(row.tool);
|
|
73
|
+
// Offline, no tool row shows an effect: printing only the rows that carry
|
|
74
|
+
// their own would read as if the others had none.
|
|
75
|
+
if (!tool) return [synopsis, '', ''];
|
|
76
|
+
return [
|
|
77
|
+
synopsis,
|
|
78
|
+
row.effect ?? tool.effect,
|
|
79
|
+
firstSentence(tool.description),
|
|
80
|
+
];
|
|
81
|
+
});
|
|
82
|
+
// One column width across both headings, so the two lists line up.
|
|
83
|
+
const render = tabulate(lines);
|
|
84
|
+
const body = (subset) => subset.map((row) => render[members.indexOf(row)]);
|
|
85
|
+
|
|
86
|
+
const out = [
|
|
87
|
+
onInstance.some((row) => row.tool)
|
|
88
|
+
? `usage: juno ${group} <verb> [id] ${INPUT_USAGE}`
|
|
89
|
+
: `usage: juno ${group} <verb>`,
|
|
90
|
+
'',
|
|
91
|
+
];
|
|
92
|
+
if (inWorkingCopy.length === 0) {
|
|
93
|
+
out.push(...body(members));
|
|
94
|
+
} else {
|
|
95
|
+
out.push('On the instance:', ...body(onInstance), '');
|
|
96
|
+
out.push(`${WORKING_COPY_HEADING}:`, ...body(inWorkingCopy));
|
|
97
|
+
}
|
|
98
|
+
if (skills?.length) {
|
|
99
|
+
const width = Math.max(...skills.map((s) => s.name.length));
|
|
100
|
+
out.push('', authoringHeading(authoring));
|
|
101
|
+
for (const s of skills) {
|
|
102
|
+
out.push(` ${s.name.padEnd(width)} ${firstSentence(s.description)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
out.push(
|
|
106
|
+
'',
|
|
107
|
+
`Run \`juno ${group} <verb> --help\` for a command's full description and input schema.`
|
|
108
|
+
);
|
|
109
|
+
return out.join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const WORKING_COPY_HEADING =
|
|
113
|
+
'In a working copy (run from the project directory)';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* workingCopyListing is every working-copy command with its effect and
|
|
117
|
+
* summary, for `juno --help`; offline, since all of it is in verbs.json.
|
|
118
|
+
*/
|
|
119
|
+
export function workingCopyListing(rows, handlerFor) {
|
|
120
|
+
return tabulate(
|
|
121
|
+
rows
|
|
122
|
+
.filter((row) => row.working_copy)
|
|
123
|
+
.map((row) => [handlerFor(row).usage, row.effect, row.summary])
|
|
124
|
+
).join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Rows of [synopsis, effect, text] as aligned, indented lines. */
|
|
128
|
+
function tabulate(lines) {
|
|
129
|
+
const width = Math.max(...lines.map(([s]) => s.length));
|
|
130
|
+
const effectWidth = Math.max(...lines.map(([, e]) => (e ?? '').length));
|
|
131
|
+
return lines.map(([synopsis, effect = '', text]) =>
|
|
132
|
+
` ${synopsis.padEnd(width)} ${(effect ?? '').padEnd(effectWidth)} ${text}`.trimEnd()
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* verbHelp is one tool command's help: the tool's full description, effect
|
|
138
|
+
* and input schema from GET /api/tools, plus what the row adds — positional,
|
|
139
|
+
* flags, fixed input. Unlike a group's, it needs the instance: the schema is
|
|
140
|
+
* the help.
|
|
141
|
+
*/
|
|
142
|
+
export async function verbHelp(row, args, io) {
|
|
143
|
+
const { target, client } = await connect(args, io);
|
|
144
|
+
const tool = (await client.listTools()).find((t) => t.name === row.tool);
|
|
145
|
+
if (!tool) {
|
|
146
|
+
throw new UsageError(
|
|
147
|
+
`${target} does not serve the tool "${row.tool}"; ` +
|
|
148
|
+
'the instance is probably older than this CLI'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const out = [`usage: ${rowUsage(row)}`, '', tool.description.trim(), ''];
|
|
153
|
+
out.push(`effect: ${row.effect ?? tool.effect}`);
|
|
154
|
+
out.push(`tool: ${row.tool}`);
|
|
155
|
+
if (row.id_field) {
|
|
156
|
+
out.push(`positional: <${row.id_field}> → input field "${row.id_field}"`);
|
|
157
|
+
} else if (row.positional === 'kind:id') {
|
|
158
|
+
out.push(
|
|
159
|
+
'positional: <kind>:<id> or <kind> <id> → input fields "kind" and "id", split at the first ":"; ' +
|
|
160
|
+
'ha:<entity_id> and mqtt:<topic> are endpoints'
|
|
161
|
+
);
|
|
162
|
+
} else if (row.positional === 'file') {
|
|
163
|
+
out.push(
|
|
164
|
+
'positional: <file> → its content under "files" by basename, and "entry"'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
for (const [flag, spec] of Object.entries(row.flags ?? {})) {
|
|
168
|
+
const repeat = spec.multiple ? ', repeatable' : '';
|
|
169
|
+
out.push(
|
|
170
|
+
`flag: --${flag} <${spec.type}${repeat}> → input field "${spec.field}"`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (row.fixed_input) {
|
|
174
|
+
out.push(`always sends: ${JSON.stringify(row.fixed_input)}`);
|
|
175
|
+
}
|
|
176
|
+
out.push('', 'input schema:', JSON.stringify(tool.input_schema, null, 2));
|
|
177
|
+
return out.join('\n');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The help of a non-tool command, offline: its summary and its usage. */
|
|
181
|
+
export function nonToolHelp(row, handler) {
|
|
182
|
+
return `${row.summary}\n\nusage: ${handler.usage}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* firstSentence is a description's first sentence, for a one-line listing:
|
|
187
|
+
* up to the first `.`, `!` or `?` followed by whitespace, skipping the ones
|
|
188
|
+
* that close an abbreviation such as "e.g.".
|
|
189
|
+
*/
|
|
190
|
+
export function firstSentence(text = '') {
|
|
191
|
+
const flat = text.trim().replace(/\s+/g, ' ');
|
|
192
|
+
const end = /[.!?](?= |$)/g;
|
|
193
|
+
let m;
|
|
194
|
+
while ((m = end.exec(flat))) {
|
|
195
|
+
const upTo = flat.slice(0, m.index + 1);
|
|
196
|
+
if (!/\b(e\.g|i\.e|etc|vs)\.$/i.test(upTo)) return upTo;
|
|
197
|
+
}
|
|
198
|
+
return flat;
|
|
199
|
+
}
|