@widgetic/cli 0.1.1 → 0.1.3
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 +4 -4
- package/dist/cli.js +39 -7
- package/dist/commands/completion.js +2 -0
- package/dist/commands/doctor.js +2 -0
- package/dist/commands/schema.js +2 -0
- package/dist/constants.js +6 -0
- package/dist/credentials.js +2 -2
- package/dist/help.js +174 -0
- package/dist/http.js +2 -2
- package/dist/mcp/session.js +2 -1
- package/dist/overlay.js +24 -0
- package/dist/rest/register.js +9 -4
- package/generated/contracts.json +1 -1
- package/generated/mcp-manifest.json +1 -1
- package/generated/rest-manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ widgetic api widgets get-widgets # a REST call
|
|
|
20
20
|
widgetic mcp create-widget --title "Launch countdown" --brief "A countdown to 14 March 2027" --wait
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`widgetic --help
|
|
23
|
+
`widgetic` on its own (or `widgetic --help`) is the menu: the commands under three headings, the first steps (make a key in the dashboard under Settings > API Keys, log in, run `doctor`) and a few examples. `widgetic api --help` lists the groups of the REST API with a line each, `widgetic mcp --help` lists the build tools by what they do to your account (read, write, publish, delete), and `widgetic api <group> --help` and `widgetic <command> --help` go one level down. A word that is no command is looked for in both: `widgetic widgets list` answers `try widgetic api widgets list`. The whole reference, one page per group, is generated into [`docs/commands`](docs/commands/index.mdx).
|
|
24
24
|
|
|
25
25
|
## The commands
|
|
26
26
|
|
|
@@ -66,7 +66,7 @@ The CLI is generated from the **public** contracts and from nothing else. `@widg
|
|
|
66
66
|
| `partner` | A partner key (`wgt_prt_…`): managed users, credits, storage, analytics | Listed. Its help says `Needs a partner API key (wgt_prt_…).` |
|
|
67
67
|
| `public` | Browsers, with no key: the runtime of an embed | Left out of `--help`; `widgetic api call <operationId>` runs it. No key is sent. |
|
|
68
68
|
|
|
69
|
-
The CLI does not decide who may do what. The API does, on every call, from the key: a key without the scope or the level gets a 403 (exit code 3), the same answer as from the SDK or from `curl`. The CLI only makes what the key may already do easy to find and to call.
|
|
69
|
+
The CLI does not decide who may do what. The API does, on every call, from the key: a key without the scope or the level gets a 403 (exit code 3), the same answer as from the SDK or from `curl`. The CLI only makes what the key may already do easy to find and to call. One thing to know when you make the key: the `widgetic mcp` commands need a key made with **full access**, because the MCP endpoint hands a client every tool the account has and refuses a key limited to some scopes (the dashboard's "Read only", "Content" and "Custom"). The `widgetic api` commands work with a limited key, within its scopes.
|
|
70
70
|
|
|
71
71
|
## The key
|
|
72
72
|
|
|
@@ -145,7 +145,7 @@ The gateway (`Services/api-gateway-service`) builds a package, [`@widgetic/api-c
|
|
|
145
145
|
There is nothing to keep in step by hand: the commands are a function of the contracts, and four guards fail when they are not.
|
|
146
146
|
|
|
147
147
|
- **Tests.** `test/command-snapshot.test.ts` holds the command tree (names, flags, groups) to a checked-in snapshot, so a new endpoint, a renamed tool or a changed flag shows up as a diff somebody reads; `test/command-reference.test.ts` does the same for the generated reference in `docs/commands`; the generator tests fail on an operation or a tool that has no command, or two with one name.
|
|
148
|
-
- **The release gate** (`npm run check:release`, run by the publish script before the version is raised, and by the `publish` job of `.gitlab-ci.yml`). It fails unless the contracts the commands were generated from are a release, are the newest `latest` of `@widgetic/api-contracts` on npm, and the tool names and the tool contract version are the ones production's MCP `tools
|
|
148
|
+
- **The release gate** (`npm run check:release`, run by the publish script before the version is raised, and by the `publish` job of `.gitlab-ci.yml`). It fails unless the contracts the commands were generated from are a release, are the newest `latest` of `@widgetic/api-contracts` on npm, and the tool names and the tool contract version are the ones production's MCP server serves. It reads that from `/.well-known/widgetic-mcp-contract` on production's MCP host, a public document (the contract version, the number of tools and their names; no key, no cookie, no description of any tool, a redirect is never followed). A document that is there but cannot be trusted (an error, something that is not JSON, another schema, a count that is not the length of the list) fails the gate: it never sends the gate to look somewhere else. Only while production runs a gateway older than the release that adds the document (it answers `404`) does the gate ask the MCP server itself, with the key of `WIDGETIC_API_KEY` or of `widgetic auth login`: `initialize`, then `tools/list`. That key has to be made with full access: the MCP endpoint refuses a key limited to some scopes (read only, content or custom), and a key for a CI variable is best made on an account that is used for nothing else. With neither the document nor a key the gate fails, and says which of the two is missing. The report says which one it relied on. Nothing is published and no version is raised when it fails.
|
|
149
149
|
- **At run time.** When an MCP command opens a session and the server's tool contract is not the one the CLI was built for, the CLI says so once in 24 hours on stderr (never with `--json`), and `widgetic doctor` always shows the two versions.
|
|
150
150
|
- **The server can refuse an old CLI.** Every request carries `X-SDK-Version: @widgetic/cli/<version>` (MCP: the client's `clientInfo`). The gateway counts the versions it sees and can answer `426` to one that is too old: exit code 7.
|
|
151
151
|
|
|
@@ -159,7 +159,7 @@ npm run check # typecheck, tests, prettier
|
|
|
159
159
|
npm run build # dist/ (and generated/)
|
|
160
160
|
npm run docs # write docs/commands from the installed contracts
|
|
161
161
|
npm run snapshot # write the command snapshot from the installed contracts
|
|
162
|
-
npm run check:release # the release gate (needs the network
|
|
162
|
+
npm run check:release # the release gate (needs the network; a key only while production publishes no contract document)
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
Tests run the real program in-process against a fake environment (`test/helpers/fake-environment.ts`): a fetch that answers from a function and records what it was asked, streams that collect the output, no terminal, no real network, and a package manager that is a function (nothing is ever installed by a test). A fake MCP server (`test/helpers/fake-mcp-server.ts`) answers the MCP tests. The question on the terminal is the one thing the fake replaces whole, so `test/terminal.test.ts` runs the real one on streams of its own.
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { registerDoctorCommand } from "./commands/doctor.js";
|
|
|
6
6
|
import { registerSchemaCommand } from "./commands/schema.js";
|
|
7
7
|
import { registerUpdateCommand } from "./commands/update.js";
|
|
8
8
|
import { CliError, EXIT_CODES, usageError } from "./errors.js";
|
|
9
|
+
import { apiHelpFooter, existingCommandPath, firstCommandWord, mcpHelpFooter, ROOT_HEADINGS, rootHelpFooter, unknownCommandHint } from "./help.js";
|
|
9
10
|
import { loadMcpManifest, loadRestManifest } from "./manifest/load.js";
|
|
10
11
|
import { registerMcpCommands } from "./mcp/register.js";
|
|
11
12
|
import { asCliError, printError } from "./output.js";
|
|
@@ -58,12 +59,19 @@ export function namespaceOf(argv) {
|
|
|
58
59
|
*/
|
|
59
60
|
export function buildProgram(environment, argv = []) {
|
|
60
61
|
const root = createRootCommand(environment);
|
|
62
|
+
// The list of commands in `widgetic --help` is a menu: each command is listed under the heading that was set when it
|
|
63
|
+
// was added, with the one line of its `summary` (the whole `description` is the help of the command itself).
|
|
64
|
+
root.commandsGroup(ROOT_HEADINGS.work);
|
|
61
65
|
const api = root
|
|
62
66
|
.command('api')
|
|
63
|
-
.
|
|
67
|
+
.summary('Call the public REST API: one command for every operation.')
|
|
68
|
+
.description('Call the public REST API: one command for every operation of its OpenAPI description.')
|
|
69
|
+
.addHelpText('after', apiHelpFooter());
|
|
64
70
|
const mcp = root
|
|
65
71
|
.command('mcp')
|
|
66
|
-
.
|
|
72
|
+
.summary('Build, edit and publish widgets with the AI build tools of the MCP server.')
|
|
73
|
+
.description('Run the AI build tools of the Widgetic MCP server: one command for every tool.')
|
|
74
|
+
.addHelpText('after', mcpHelpFooter());
|
|
67
75
|
const namespace = namespaceOf(argv);
|
|
68
76
|
if (namespace === 'api') {
|
|
69
77
|
const manifest = loadRestManifest();
|
|
@@ -75,29 +83,53 @@ export function buildProgram(environment, argv = []) {
|
|
|
75
83
|
registerMcpCommands(mcp, manifest, environment);
|
|
76
84
|
registerMcpCall(mcp, manifest, { environment, manifest });
|
|
77
85
|
}
|
|
86
|
+
root.commandsGroup(ROOT_HEADINGS.setup);
|
|
78
87
|
registerAuthCommands(root, environment);
|
|
79
88
|
registerDoctorCommand(root, environment);
|
|
80
89
|
registerUpdateCommand(root, environment);
|
|
81
|
-
registerSchemaCommand(root, environment);
|
|
82
90
|
registerCompletionCommands(root, environment, (words) => buildProgram(environment, words));
|
|
91
|
+
root.commandsGroup(ROOT_HEADINGS.lookup);
|
|
92
|
+
registerSchemaCommand(root, environment);
|
|
93
|
+
root.helpCommand('help [command]', 'Show the help of a command');
|
|
94
|
+
root.addHelpText('after', rootHelpFooter());
|
|
83
95
|
return root;
|
|
84
96
|
}
|
|
85
|
-
/**
|
|
86
|
-
|
|
97
|
+
/**
|
|
98
|
+
* What a Commander error becomes: `null` when it only means "the help or the version was printed". A word that is no
|
|
99
|
+
* command of `widgetic` itself gets a hint that looks for it in the REST API and among the tools (`widgetic widgets
|
|
100
|
+
* list` was meant as `widgetic api widgets list`); one that is no command under `api` or `mcp` keeps Commander's own
|
|
101
|
+
* "did you mean", which is right there, and is told which help lists the commands of that level.
|
|
102
|
+
*/
|
|
103
|
+
function fromCommanderError(error, argv, program) {
|
|
87
104
|
if (CLEAN_COMMANDER_CODES.has(error.code))
|
|
88
105
|
return null;
|
|
89
106
|
const message = error.message.replace(/^error:\s*/i, '');
|
|
107
|
+
if (error.code === 'commander.unknownCommand') {
|
|
108
|
+
const word = /^unknown command '([^']*)'/.exec(message)?.[1];
|
|
109
|
+
const first = firstCommandWord(argv);
|
|
110
|
+
if (word !== undefined && word === first) {
|
|
111
|
+
return usageError(message, unknownCommandHint(word, argv.slice(argv.indexOf(first) + 1)));
|
|
112
|
+
}
|
|
113
|
+
// Under `api` or `mcp`: Commander has said what is close; the hint says where the commands of that level are listed.
|
|
114
|
+
if (word !== undefined && program !== undefined) {
|
|
115
|
+
const where = ['widgetic', ...existingCommandPath(program, argv)].join(' ');
|
|
116
|
+
return usageError(message, `Run ${where} --help to see its commands.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
90
119
|
return usageError(message, 'Run the command with --help to see what it takes.');
|
|
91
120
|
}
|
|
92
121
|
export async function run(argv, environment) {
|
|
93
122
|
const json = argv.includes('--json');
|
|
123
|
+
let program;
|
|
94
124
|
try {
|
|
95
|
-
|
|
125
|
+
program = buildProgram(environment, argv);
|
|
96
126
|
await program.parseAsync([...argv], { from: 'user' });
|
|
97
127
|
return EXIT_CODES.ok;
|
|
98
128
|
}
|
|
99
129
|
catch (error) {
|
|
100
|
-
const failure = error instanceof CommanderError
|
|
130
|
+
const failure = error instanceof CommanderError
|
|
131
|
+
? fromCommanderError(error, argv, program)
|
|
132
|
+
: asCliError(error, environment);
|
|
101
133
|
if (failure === null)
|
|
102
134
|
return EXIT_CODES.ok;
|
|
103
135
|
printError(environment, failure, json);
|
|
@@ -121,6 +121,8 @@ export function completionsFor(build, words, current) {
|
|
|
121
121
|
export function registerCompletionCommands(root, environment, build) {
|
|
122
122
|
root
|
|
123
123
|
.command('completion')
|
|
124
|
+
// The line the list of commands of `widgetic --help` shows; the shell lines are the help of `completion` itself.
|
|
125
|
+
.summary('Print the script that completes commands and flags with the Tab key.')
|
|
124
126
|
.description('Print the script that completes widgetic commands and flags with the Tab key.\n\nbash: eval "$(widgetic completion bash)" in ~/.bashrc\nzsh: eval "$(widgetic completion zsh)" in ~/.zshrc, after compinit\nfish: widgetic completion fish > ~/.config/fish/completions/widgetic.fish')
|
|
125
127
|
.argument('<shell>', `One of: ${SHELLS.join(', ')}`)
|
|
126
128
|
.action((shell) => {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -283,6 +283,8 @@ async function runDoctor(environment, node) {
|
|
|
283
283
|
export function registerDoctorCommand(root, environment) {
|
|
284
284
|
const doctor = root
|
|
285
285
|
.command('doctor')
|
|
286
|
+
// The line the list of commands of `widgetic --help` shows; the whole text is the help of `doctor` itself.
|
|
287
|
+
.summary('Check the install, the API key and both servers, and say what is wrong.')
|
|
286
288
|
.description('Check the install and both servers: the CLI and its contracts, the API key, the REST API, and the MCP server (which tool contract it speaks and which tools it offers, compared with this CLI).\n\nExit code 0 when nothing failed, 1 when a check did; --json prints every check.');
|
|
287
289
|
addBasicOptions(doctor, { server: true });
|
|
288
290
|
doctor.action(async () => {
|
package/dist/commands/schema.js
CHANGED
|
@@ -183,6 +183,8 @@ export function unknownOperation(manifest, words) {
|
|
|
183
183
|
export function registerSchemaCommand(root, environment) {
|
|
184
184
|
const schema = root
|
|
185
185
|
.command('schema')
|
|
186
|
+
// The line the list of commands of `widgetic --help` shows; the whole text is the help of `schema` itself.
|
|
187
|
+
.summary('Show everything one command takes, with the schema its input is checked against.')
|
|
186
188
|
.description('Print the whole description of one command and the schema its input is checked against.\n\nIt is the cheap way to find out what a command takes, for a person and for an agent: one command, not the whole API.');
|
|
187
189
|
const api = schema
|
|
188
190
|
.command('api')
|
package/dist/constants.js
CHANGED
|
@@ -12,6 +12,12 @@ export const DEFAULT_PROFILE = 'default';
|
|
|
12
12
|
export const PROFILE_NAME = /^[A-Za-z0-9._-]{1,64}$/;
|
|
13
13
|
/** What a Widgetic API key looks like (`wgt_usr_…`, `wgt_prt_…`, `wdg_org_…`): used to warn about a value that is not one, never to refuse one. */
|
|
14
14
|
export const API_KEY_SHAPE = /^(?:wgt_(?:adm|prt|usr)|wdg_org)_[A-Za-z0-9]{16,}$/;
|
|
15
|
+
/**
|
|
16
|
+
* What a person is told to do about the API key: with none at all, to make one in the dashboard and give it to the CLI;
|
|
17
|
+
* with one the server does not accept, to give a working one. Both name where the keys are (Settings > API Keys).
|
|
18
|
+
*/
|
|
19
|
+
export const NO_KEY_HINT = 'Make an API key in the dashboard (Settings > API Keys), then run `widgetic auth login` with it, or set WIDGETIC_API_KEY.';
|
|
20
|
+
export const KEY_REFUSED_HINT = 'Run `widgetic auth login` with a working API key (Settings > API Keys in the dashboard), or set WIDGETIC_API_KEY.';
|
|
15
21
|
/** Seconds a call may take before the CLI gives up on it; `--timeout` changes it. */
|
|
16
22
|
export const DEFAULT_TIMEOUT_SECONDS = 60;
|
|
17
23
|
/**
|
package/dist/credentials.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { DEFAULT_PROFILE } from "./constants.js";
|
|
2
|
+
import { DEFAULT_PROFILE, NO_KEY_HINT } from "./constants.js";
|
|
3
3
|
import { CliError, EXIT_CODES } from "./errors.js";
|
|
4
4
|
const FILE_NAME = 'credentials.json';
|
|
5
5
|
function damaged(path, why) {
|
|
@@ -93,7 +93,7 @@ export function noApiKey(settings) {
|
|
|
93
93
|
? 'No API key.'
|
|
94
94
|
: `No API key is saved for the profile "${settings.profile}".`,
|
|
95
95
|
exitCode: EXIT_CODES.auth,
|
|
96
|
-
hint:
|
|
96
|
+
hint: NO_KEY_HINT
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
/** The key for a call that carries one, or the error that says how to get one (exit 3, nothing was sent). */
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { editDistance } from "./commands/schema.js";
|
|
2
|
+
import { loadMcpManifest, loadRestManifest } from "./manifest/load.js";
|
|
3
|
+
import { API_GROUP_SUMMARIES } from "./overlay.js";
|
|
4
|
+
/**
|
|
5
|
+
* What the screens a new person meets first say beyond what the contracts say: the headings that sort the commands of
|
|
6
|
+
* `widgetic --help`, the "Get started" and "Examples" at the foot of `widgetic`, `widgetic api` and `widgetic mcp`, the
|
|
7
|
+
* line each group of the REST API gets, and what is said after a word that is no command.
|
|
8
|
+
*
|
|
9
|
+
* The commands themselves, their flags and their descriptions come from the contracts and are not here. Every command
|
|
10
|
+
* an example names is checked by `test/help.test.ts` (it exists, and so does every flag), so the help cannot send
|
|
11
|
+
* a person to a command that was renamed. The lines are short on purpose (78 characters at most): the text of a
|
|
12
|
+
* footer is printed as it is written, and a terminal that is 80 columns wide would break a longer one.
|
|
13
|
+
*/
|
|
14
|
+
/** The headings that sort the commands of `widgetic --help`: what you do with Widgetic, how you set it up, how you look things up. */
|
|
15
|
+
export const ROOT_HEADINGS = {
|
|
16
|
+
work: 'Work with Widgetic:',
|
|
17
|
+
setup: 'Set up:',
|
|
18
|
+
lookup: 'Look things up:'
|
|
19
|
+
};
|
|
20
|
+
/** The dashboard, where an API key is made (Settings > API Keys): the address the documentation gives for it. */
|
|
21
|
+
const DASHBOARD = 'app.widgetic.com';
|
|
22
|
+
/** The first steps of a person who has just installed the CLI: the first is made in the dashboard, the rest are commands. */
|
|
23
|
+
export const GET_STARTED_COMMANDS = [
|
|
24
|
+
{ command: 'widgetic auth login', says: 'save it (it is asked for, and never shown)' },
|
|
25
|
+
{ command: 'widgetic doctor', says: 'check that the key and both servers work' },
|
|
26
|
+
{ command: 'widgetic mcp list-widgets', says: 'see your widgets' }
|
|
27
|
+
];
|
|
28
|
+
/** Commands that are safe to run to see what the CLI does: they read, or they only print the request. */
|
|
29
|
+
export const ROOT_EXAMPLES = [
|
|
30
|
+
'widgetic mcp list-widgets',
|
|
31
|
+
'widgetic api widgets list --json',
|
|
32
|
+
'widgetic mcp create-widget --title Launch --brief "A countdown" --dry-run',
|
|
33
|
+
'widgetic schema mcp create-widget'
|
|
34
|
+
];
|
|
35
|
+
export const API_EXAMPLES = [
|
|
36
|
+
'widgetic api widgets list',
|
|
37
|
+
'widgetic api compositions list --size 5',
|
|
38
|
+
'widgetic api tokens validate-token'
|
|
39
|
+
];
|
|
40
|
+
export const MCP_EXAMPLES = [
|
|
41
|
+
'widgetic mcp list-widgets',
|
|
42
|
+
'widgetic mcp create-widget --title Launch --brief "A countdown" --wait',
|
|
43
|
+
'widgetic mcp create-widget --help'
|
|
44
|
+
];
|
|
45
|
+
/** A block of lines indented by two spaces, under a heading. */
|
|
46
|
+
function section(heading, lines) {
|
|
47
|
+
return `${heading}\n${lines.map((line) => ` ${line}`).join('\n')}`;
|
|
48
|
+
}
|
|
49
|
+
/** The numbered steps of "Get started", with the sentence of each command aligned in a column. */
|
|
50
|
+
function stepLines() {
|
|
51
|
+
const width = Math.max(...GET_STARTED_COMMANDS.map((step) => step.command.length));
|
|
52
|
+
return [
|
|
53
|
+
`1. Make an API key in the dashboard (Settings > API Keys, ${DASHBOARD}).`,
|
|
54
|
+
...GET_STARTED_COMMANDS.map((step, index) => `${index + 2}. ${step.command.padEnd(width)} ${step.says}`)
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
/** What `widgetic --help` ends with. */
|
|
58
|
+
export function rootHelpFooter() {
|
|
59
|
+
return [
|
|
60
|
+
'',
|
|
61
|
+
section('Get started:', stepLines()),
|
|
62
|
+
'',
|
|
63
|
+
section('Examples:', ROOT_EXAMPLES),
|
|
64
|
+
'',
|
|
65
|
+
'Add --json to get the result as one line of JSON, for scripts. Add --dry-run',
|
|
66
|
+
'to a command that calls the API to print the request and send nothing.',
|
|
67
|
+
'Add --help to any command to see what it takes.'
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
/** What `widgetic api --help` ends with. */
|
|
71
|
+
export function apiHelpFooter() {
|
|
72
|
+
return [
|
|
73
|
+
'',
|
|
74
|
+
section('Examples:', API_EXAMPLES),
|
|
75
|
+
'',
|
|
76
|
+
'Run widgetic api <group> --help to list the commands of a group. A group',
|
|
77
|
+
'that needs a partner API key says so.'
|
|
78
|
+
].join('\n');
|
|
79
|
+
}
|
|
80
|
+
/** What `widgetic mcp --help` ends with. */
|
|
81
|
+
export function mcpHelpFooter() {
|
|
82
|
+
return [
|
|
83
|
+
'',
|
|
84
|
+
section('Examples:', MCP_EXAMPLES),
|
|
85
|
+
'',
|
|
86
|
+
'A command that builds a widget starts the build and answers at once; --wait',
|
|
87
|
+
'keeps asking until the build is finished and prints the finished widget.'
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
90
|
+
/** Every command of a group needs a partner API key (`wgt_prt_…`), so the group's line says so. */
|
|
91
|
+
function needsPartnerKey(group) {
|
|
92
|
+
const listed = group.commands.filter((command) => !command.hidden);
|
|
93
|
+
return listed.length > 0 && listed.every((command) => command.accessLevel === 'partner');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The line a group of the REST API gets in the list of `widgetic api --help` and at the top of its own help: what it
|
|
97
|
+
* works on, from the overlay, and the kind of key it needs when that is always the partner's. A group the overlay does
|
|
98
|
+
* not know (a later contract added it) is listed by its name and its number of commands, which is true if not pretty.
|
|
99
|
+
*/
|
|
100
|
+
export function apiGroupSummary(group) {
|
|
101
|
+
const count = group.commands.length;
|
|
102
|
+
const written = API_GROUP_SUMMARIES[group.name] ?? `${group.title}: ${count} ${count === 1 ? 'command' : 'commands'}.`;
|
|
103
|
+
return needsPartnerKey(group) ? `${written.replace(/\.$/, '')} (needs a partner key).` : written;
|
|
104
|
+
}
|
|
105
|
+
/** A name without its separators or its case, so `list_widgets`, `list-widgets` and `ListWidgets` are one word. */
|
|
106
|
+
const alphanumeric = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
107
|
+
/**
|
|
108
|
+
* The commands of `argv` that exist, from the program down: `api widgets` for `api widgets get`, where `get` is not a
|
|
109
|
+
* command of `widgets`. A word that is not a command (a flag's value, the unknown word itself) ends the walk, so what
|
|
110
|
+
* is returned is always a command the person can ask for `--help`.
|
|
111
|
+
*/
|
|
112
|
+
export function existingCommandPath(root, argv) {
|
|
113
|
+
let command = root;
|
|
114
|
+
const path = [];
|
|
115
|
+
for (const word of argv) {
|
|
116
|
+
if (word.startsWith('-'))
|
|
117
|
+
continue;
|
|
118
|
+
const next = command.commands.find((child) => child.name() === word || child.aliases().includes(word));
|
|
119
|
+
if (!next)
|
|
120
|
+
break;
|
|
121
|
+
command = next;
|
|
122
|
+
path.push(next.name());
|
|
123
|
+
}
|
|
124
|
+
return path;
|
|
125
|
+
}
|
|
126
|
+
/** The word of a command line that is the name of the command: the first one that is not a flag. */
|
|
127
|
+
export function firstCommandWord(argv) {
|
|
128
|
+
return argv.find((word) => !word.startsWith('-'));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* What to say after `widgetic <word>` when the word is no command of `widgetic`. People coming from other tools leave
|
|
132
|
+
* out the namespace (`widgetic widgets list`, `widgetic list-widgets`), so the word is looked for among the groups of
|
|
133
|
+
* the REST API and the tools of the MCP server, and a name that is close to one is offered. One line: the hint is
|
|
134
|
+
* printed on a single indented line.
|
|
135
|
+
*/
|
|
136
|
+
export function unknownCommandHint(word, rest = []) {
|
|
137
|
+
const fallback = 'Run widgetic --help to see the commands.';
|
|
138
|
+
try {
|
|
139
|
+
const groups = loadRestManifest().groups.filter((group) => !group.hidden);
|
|
140
|
+
const tools = loadMcpManifest().commands;
|
|
141
|
+
const typed = alphanumeric(word);
|
|
142
|
+
const group = groups.find((candidate) => alphanumeric(candidate.name) === typed);
|
|
143
|
+
if (group) {
|
|
144
|
+
// The words the person typed after the group, up to the first flag (`widgets list --id x` is `list`).
|
|
145
|
+
const following = [];
|
|
146
|
+
for (const next of rest) {
|
|
147
|
+
if (next.startsWith('-'))
|
|
148
|
+
break;
|
|
149
|
+
following.push(next);
|
|
150
|
+
}
|
|
151
|
+
return `"${word}" is in the REST API: try widgetic api ${[group.name, ...following].join(' ')}`;
|
|
152
|
+
}
|
|
153
|
+
const tool = tools.find((candidate) => alphanumeric(candidate.name) === typed);
|
|
154
|
+
if (tool)
|
|
155
|
+
return `"${word}" is a tool of the MCP server: try widgetic mcp ${tool.name}`;
|
|
156
|
+
// Only names a typo away: a name that merely contains the word (`widget` is in a dozen tools) would bury the one meant.
|
|
157
|
+
const allowed = typed.length >= 5 ? 2 : 1;
|
|
158
|
+
const closeTo = (names) => names
|
|
159
|
+
.map((name) => ({ name, distance: editDistance(typed, alphanumeric(name)) }))
|
|
160
|
+
.filter((entry) => entry.distance <= allowed)
|
|
161
|
+
.sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name))
|
|
162
|
+
.map((entry) => entry.name);
|
|
163
|
+
const near = [
|
|
164
|
+
...closeTo(groups.map((candidate) => candidate.name)).map((name) => `widgetic api ${name}`),
|
|
165
|
+
...closeTo(tools.map((candidate) => candidate.name)).map((name) => `widgetic mcp ${name}`)
|
|
166
|
+
].slice(0, 3);
|
|
167
|
+
if (near.length > 0)
|
|
168
|
+
return `Did you mean ${near.join(' or ')}? ${fallback}`;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// The manifests could not be read (a broken install): `widgetic doctor` says so; the hint is only a courtesy.
|
|
172
|
+
}
|
|
173
|
+
return fallback;
|
|
174
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { KEY_REFUSED_HINT } from "./constants.js";
|
|
1
2
|
import { CliError, EXIT_CODES, exitCodeForStatus } from "./errors.js";
|
|
2
3
|
export function isRecord(value) {
|
|
3
4
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -38,7 +39,6 @@ function upgradeHint(details) {
|
|
|
38
39
|
// The gateway names the package of the client it recognised; for this CLI that is `@widgetic/cli`.
|
|
39
40
|
return typeof command === 'string' ? `Update the CLI: ${command}` : undefined;
|
|
40
41
|
}
|
|
41
|
-
const NO_KEY_HINT = 'Run `widgetic auth login` with an API key, or set WIDGETIC_API_KEY.';
|
|
42
42
|
/** The error an answer that was not a success stands for. */
|
|
43
43
|
export function errorFromAnswer(answer, now, keySource) {
|
|
44
44
|
const { status, headers, body } = answer;
|
|
@@ -69,7 +69,7 @@ export function errorFromAnswer(answer, now, keySource) {
|
|
|
69
69
|
hint =
|
|
70
70
|
keySource === 'environment'
|
|
71
71
|
? 'The key in WIDGETIC_API_KEY was not accepted: check it, or unset it to use the saved one.'
|
|
72
|
-
:
|
|
72
|
+
: KEY_REFUSED_HINT;
|
|
73
73
|
}
|
|
74
74
|
else if (status === 426) {
|
|
75
75
|
hint = upgradeHint(details) ?? suggested;
|
package/dist/mcp/session.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { KEY_REFUSED_HINT } from "../constants.js";
|
|
1
2
|
import { CliError, EXIT_CODES, exitCodeForStatus } from "../errors.js";
|
|
2
3
|
import { errorFromAnswer, isRecord, networkFailure, readAnswer } from "../http.js";
|
|
3
4
|
import { cliVersion, identityHeaders } from "../version.js";
|
|
@@ -24,7 +25,7 @@ export function refusalOf(outcome) {
|
|
|
24
25
|
code: 'AUTH_REQUIRED',
|
|
25
26
|
message: outcome.text || 'The API key was not accepted.',
|
|
26
27
|
exitCode: EXIT_CODES.auth,
|
|
27
|
-
hint:
|
|
28
|
+
hint: KEY_REFUSED_HINT
|
|
28
29
|
});
|
|
29
30
|
}
|
|
30
31
|
return new CliError({
|
package/dist/overlay.js
CHANGED
|
@@ -29,3 +29,27 @@ export const WAIT_OVERLAY = {
|
|
|
29
29
|
export function isWaitTool(tool) {
|
|
30
30
|
return WAIT_OVERLAY.tools.includes(tool);
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The line `widgetic api --help` shows beside each group of the REST API. The contract names the groups (they are the
|
|
34
|
+
* tags of its operations) and says nothing about them, so without these lines the list would be thirteen bare nouns and
|
|
35
|
+
* a person could not tell `compositions` from `canvases`. Each line is written from what the operations of the group
|
|
36
|
+
* say about themselves; a group that has none yet (a later contract adds one) is listed with the number of its commands
|
|
37
|
+
* and `test/overlay.test.ts` fails until somebody writes its line.
|
|
38
|
+
*/
|
|
39
|
+
export const API_GROUP_SUMMARIES = {
|
|
40
|
+
'brand-kit': 'Your Brand Kit: the colors, fonts, icons, corner radius and notes for the AI that every new widget follows.',
|
|
41
|
+
canvases: 'The canvases that hold your widgets: create, list, read, update and delete them.',
|
|
42
|
+
compositions: 'What is made from a widget: its content, design, preview, versions, data and embed settings.',
|
|
43
|
+
credits: "An account's credit balance and credit ledger.",
|
|
44
|
+
entitlements: 'The entitlements of a partner and of its managed users.',
|
|
45
|
+
invoices: "A partner's invoices.",
|
|
46
|
+
'managed-users': "A partner's managed users: their limits, design defaults and editor sessions.",
|
|
47
|
+
operations: 'The status of work that goes on after the call that started it has answered.',
|
|
48
|
+
partners: 'A partner account: analytics, billing, presets, notifications, storage and data.',
|
|
49
|
+
'public-embed': 'What the browser of a visitor calls to show and run a published widget; it needs no API key. Left out of the list.',
|
|
50
|
+
tokens: 'Check that the API key in use is valid.',
|
|
51
|
+
uploads: 'Upload files, list and delete them, and see the upload limits.',
|
|
52
|
+
users: 'Your account: API keys, the websites that may show your widgets, and adapter connections.',
|
|
53
|
+
widgets: 'Your library of widgets: list and create them, and start a composition from one.',
|
|
54
|
+
workflows: 'Workflows: create, edit, publish, pause and archive them, and restore their versions.'
|
|
55
|
+
};
|
package/dist/rest/register.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolveSettings, timeoutMilliseconds } from "../config.js";
|
|
2
2
|
import { confirmOrStop } from "../confirm.js";
|
|
3
3
|
import { requireApiKey } from "../credentials.js";
|
|
4
|
-
import {
|
|
4
|
+
import { apiGroupSummary } from "../help.js";
|
|
5
|
+
import { addCommonOptions, addFlag, commandHelp, readCommon, readFlags } from "../options.js";
|
|
5
6
|
import { note, printResult } from "../output.js";
|
|
6
7
|
import { retryNotifier } from "../retry.js";
|
|
7
8
|
import { sendRestRequest } from "./execute.js";
|
|
@@ -125,14 +126,18 @@ function registerCommand(parent, group, command, run) {
|
|
|
125
126
|
await runRestCommand(run, command, node, bindings);
|
|
126
127
|
});
|
|
127
128
|
}
|
|
128
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Adds the groups of the manifest, and their commands, under `api`. A group is listed with the line of
|
|
131
|
+
* `apiGroupSummary` (what it works on, and the key it needs when that is a partner's), which is also the first line
|
|
132
|
+
* of its own help: the contract names the group and says nothing else about it.
|
|
133
|
+
*/
|
|
129
134
|
export function registerRestCommands(api, manifest, environment) {
|
|
130
135
|
const run = { environment, manifest };
|
|
131
136
|
for (const group of manifest.groups) {
|
|
132
137
|
const node = api
|
|
133
138
|
.command(group.name, { hidden: group.hidden })
|
|
134
|
-
.summary(group
|
|
135
|
-
.description(
|
|
139
|
+
.summary(apiGroupSummary(group))
|
|
140
|
+
.description(apiGroupSummary(group));
|
|
136
141
|
for (const command of group.commands)
|
|
137
142
|
registerCommand(node, group, command, run);
|
|
138
143
|
}
|
package/generated/contracts.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"package":"@widgetic/api-contracts","version":"1.11.
|
|
1
|
+
{"package":"@widgetic/api-contracts","version":"1.11.111","publishTag":"latest","source":{"commit":"fa75adf8384a27c620fb9d0d99b65db4f25ca7b5","branch":null,"dirty":false,"atOriginMain":true},"openApi":{"file":"openapi-public.json","sha256":"f268893f66578ba4099660fb42b4f8f1c3c56bc0672f1190b4bb12b905f34e6c","paths":124,"operations":169,"servers":["https://api.widgetic.com/v1"]},"mcp":{"file":"mcp-tools.json","sha256":"e6ca36563883e9d31597f201bcc0276babede3b901310558d291bff349ddbae3","contractVersion":"1.14.0","serverName":"widgetic","tools":34}}
|