@widgetic/cli 0.0.0-stage → 0.1.1
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/LICENSE +122 -0
- package/README.md +184 -2
- package/bin/widgetic.js +5 -0
- package/dist/cli.js +106 -0
- package/dist/commands/auth.js +269 -0
- package/dist/commands/call.js +97 -0
- package/dist/commands/completion.js +148 -0
- package/dist/commands/doctor.js +291 -0
- package/dist/commands/schema.js +214 -0
- package/dist/commands/update.js +159 -0
- package/dist/commands/verify.js +64 -0
- package/dist/config.js +98 -0
- package/dist/confirm.js +15 -0
- package/dist/constants.js +51 -0
- package/dist/contract-note.js +42 -0
- package/dist/credentials.js +111 -0
- package/dist/environment.js +150 -0
- package/dist/errors.js +67 -0
- package/dist/http.js +122 -0
- package/dist/input.js +37 -0
- package/dist/main.js +30 -0
- package/dist/manifest/load.js +39 -0
- package/dist/manifest/types.js +14 -0
- package/dist/mcp/plan.js +32 -0
- package/dist/mcp/register.js +112 -0
- package/dist/mcp/session.js +185 -0
- package/dist/mcp/wait.js +68 -0
- package/dist/options.js +228 -0
- package/dist/output.js +90 -0
- package/dist/overlay.js +31 -0
- package/dist/patterns.js +11 -0
- package/dist/rest/execute.js +67 -0
- package/dist/rest/mime.js +37 -0
- package/dist/rest/plan.js +185 -0
- package/dist/rest/register.js +139 -0
- package/dist/retry.js +49 -0
- package/dist/secrets.js +149 -0
- package/dist/state.js +37 -0
- package/dist/update/installation.js +83 -0
- package/dist/update/lookup.js +128 -0
- package/dist/update/offer.js +165 -0
- package/dist/update/perform.js +33 -0
- package/dist/update/registry.js +68 -0
- package/dist/update/version.js +39 -0
- package/dist/validate.js +93 -0
- package/dist/version.js +59 -0
- package/generated/contracts.json +1 -0
- package/generated/mcp-manifest.json +1 -0
- package/generated/rest-manifest.json +1 -0
- package/package.json +56 -5
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { configDirectory, updateCheckSwitchedOffBy } from "../config.js";
|
|
2
|
+
import { UPDATE_CHECK_CONFIG } from "../constants.js";
|
|
3
|
+
import { EXIT_CODES } from "../errors.js";
|
|
4
|
+
import { debug, note } from "../output.js";
|
|
5
|
+
import { cliPackageName, cliVersion } from "../version.js";
|
|
6
|
+
import { describeInstallation, updateCommand } from "./installation.js";
|
|
7
|
+
import { isSnoozed, lookUpLatestVersion, saveUpdateRecord } from "./lookup.js";
|
|
8
|
+
import { performUpdate } from "./perform.js";
|
|
9
|
+
import { isNewerVersion } from "./version.js";
|
|
10
|
+
/**
|
|
11
|
+
* The routine part of updating: while a command runs, the CLI finds out whether npm has a newer version; when the
|
|
12
|
+
* command has finished, it says so, shows both versions and asks whether to update.
|
|
13
|
+
*
|
|
14
|
+
* Two promises are kept so that this can never get in the way of the work:
|
|
15
|
+
*
|
|
16
|
+
* - It changes nothing about the command. The look for the newest version starts before the command and runs beside
|
|
17
|
+
* it (a 2 s limit, and the answer is kept for a day); the question comes after the command has printed its result and
|
|
18
|
+
* its exit code is final, and the exit code is never changed by what is answered or by anything that goes wrong here.
|
|
19
|
+
* - It asks only a person. Not with `--json`, `--yes`, `CI`, `NO_UPDATE_NOTIFIER` or `WIDGETIC_NO_UPDATE_CHECK`, not
|
|
20
|
+
* when any of the three streams is not a terminal, not for `--help`, `--version`, `help`, `completion` (the
|
|
21
|
+
* shell calls it on every Tab) or `update` itself, and not for a copy that is a source checkout or was fetched by
|
|
22
|
+
* `npx` to run once. `WIDGETIC_DEBUG=1` says which of these applied.
|
|
23
|
+
*/
|
|
24
|
+
/** Commands that only print text that is wanted (or are called by a program), and are never followed by a question. */
|
|
25
|
+
const QUIET_COMMANDS = new Set(['help', 'completion', '__complete', 'update']);
|
|
26
|
+
const QUIET_FLAGS = new Set(['--help', '-h', '--version', '-V']);
|
|
27
|
+
/** `widgetic`, `widgetic --help`, `widgetic completion bash`, `widgetic update`: the arguments of a run that never asks. */
|
|
28
|
+
export function isQuietInvocation(argv) {
|
|
29
|
+
if (argv.length === 0)
|
|
30
|
+
return true;
|
|
31
|
+
if (argv.some((word) => QUIET_FLAGS.has(word)))
|
|
32
|
+
return true;
|
|
33
|
+
const command = argv.find((word) => !word.startsWith('-'));
|
|
34
|
+
return command === undefined || QUIET_COMMANDS.has(command);
|
|
35
|
+
}
|
|
36
|
+
/** Why this run neither looks for an update nor asks about one, or `undefined` when it may. */
|
|
37
|
+
export function whyNoUpdateCheck(environment, argv) {
|
|
38
|
+
const switchedOffBy = updateCheckSwitchedOffBy(environment);
|
|
39
|
+
if (switchedOffBy !== undefined)
|
|
40
|
+
return `${switchedOffBy} is set`;
|
|
41
|
+
if (isQuietInvocation(argv)) {
|
|
42
|
+
return 'the command only prints help, a version, a completion or updates';
|
|
43
|
+
}
|
|
44
|
+
if (argv.includes('--json') || argv.includes('--yes')) {
|
|
45
|
+
return '--json or --yes says that nobody is there to ask';
|
|
46
|
+
}
|
|
47
|
+
if (environment.confirm === undefined || environment.stdout.isTTY !== true) {
|
|
48
|
+
return 'input, output and errors are not all a terminal';
|
|
49
|
+
}
|
|
50
|
+
if (environment.env.TERM === 'dumb')
|
|
51
|
+
return 'the terminal is dumb';
|
|
52
|
+
const installation = describeInstallation(environment.packageFolder);
|
|
53
|
+
if (installation.kind === 'checkout')
|
|
54
|
+
return 'this is a source checkout, not an installed package';
|
|
55
|
+
if (installation.kind === 'transient')
|
|
56
|
+
return 'this copy was fetched to run once (npx, dlx, bunx), not installed';
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Starts the look, or says (under `WIDGETIC_DEBUG`) why it is not made. Call it before the command, hand the result to
|
|
61
|
+
* `offerUpdate` after it.
|
|
62
|
+
*/
|
|
63
|
+
export function startUpdateCheck(environment, argv) {
|
|
64
|
+
try {
|
|
65
|
+
const reason = whyNoUpdateCheck(environment, argv);
|
|
66
|
+
if (reason !== undefined) {
|
|
67
|
+
debug(environment, `no look for a newer version: ${reason}`);
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return lookUpLatestVersion(environment, {
|
|
71
|
+
force: false,
|
|
72
|
+
timeoutMilliseconds: UPDATE_CHECK_CONFIG.BACKGROUND_TIMEOUT_MILLISECONDS
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Looking for an update must never stop the command it comes with from running.
|
|
77
|
+
debug(environment, `the look for a newer version could not start: ${error.message}`);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** What to tell someone whose copy the CLI cannot update itself. */
|
|
82
|
+
function manualInstructions(installation, latest) {
|
|
83
|
+
const command = updateCommand('npm', cliPackageName(), latest).text;
|
|
84
|
+
const where = installation.kind === 'unknown'
|
|
85
|
+
? 'It is not a global install that the CLI knows how to update (npm, pnpm, yarn or bun).'
|
|
86
|
+
: 'It cannot be updated from here.';
|
|
87
|
+
return `${where} Update it the way you installed it, for example: ${command} (without -g inside a project).`;
|
|
88
|
+
}
|
|
89
|
+
/** Remembers that the person was asked about `version`: a "no" is not asked again for a week. */
|
|
90
|
+
async function rememberAsked(environment, record, version) {
|
|
91
|
+
await saveUpdateRecord(environment, configDirectory(environment), {
|
|
92
|
+
checkedAt: record?.checkedAt ?? environment.now().toISOString(),
|
|
93
|
+
ok: record?.ok ?? true,
|
|
94
|
+
...(record?.latest !== undefined ? { latest: record.latest } : {}),
|
|
95
|
+
notifiedVersion: version,
|
|
96
|
+
notifiedAt: environment.now().toISOString()
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** Shows both versions and asks. The answer (or a silence, or Ctrl-C) never reaches the exit code of the command. */
|
|
100
|
+
async function presentOffer(environment, knowledge, current, latest) {
|
|
101
|
+
const confirm = environment.confirm;
|
|
102
|
+
if (confirm === undefined)
|
|
103
|
+
return;
|
|
104
|
+
const packageName = cliPackageName();
|
|
105
|
+
const installation = describeInstallation(environment.packageFolder);
|
|
106
|
+
note(environment, `\nA new version of the Widgetic CLI is available: ${current} → ${latest}`);
|
|
107
|
+
if (installation.kind !== 'global') {
|
|
108
|
+
note(environment, manualInstructions(installation, latest));
|
|
109
|
+
await rememberAsked(environment, knowledge.record, latest);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const command = updateCommand(installation.manager, packageName, latest);
|
|
113
|
+
let agreed = false;
|
|
114
|
+
try {
|
|
115
|
+
agreed = await confirm(`Update now? (runs \`${command.text}\`)`, {
|
|
116
|
+
timeoutMilliseconds: UPDATE_CHECK_CONFIG.PROMPT_TIMEOUT_SECONDS * 1000
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// Ctrl-C or Ctrl-D at the question is "no": the command already did its work.
|
|
121
|
+
debug(environment, `the question was not answered: ${error.message}`);
|
|
122
|
+
note(environment, '');
|
|
123
|
+
}
|
|
124
|
+
// Asked once: yes, no, silence and a failed install all mean the same question is not repeated for a week.
|
|
125
|
+
await rememberAsked(environment, knowledge.record, latest);
|
|
126
|
+
if (!agreed) {
|
|
127
|
+
note(environment, `Not updating. \`widgetic update\` does it whenever you want; this question comes back in ${UPDATE_CHECK_CONFIG.SNOOZE_DAYS} days, or for a newer version.`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await performUpdate(environment, { command, packageName, current, latest, quiet: false });
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* After the command: say that a newer version exists and ask, when this is the moment for it. `exitCode` is the
|
|
134
|
+
* command's own: 7 means the server refused this version as too old, which is always worth saying, so then npm is
|
|
135
|
+
* asked again (the saved answer may be a day old) and a "no" from last time does not silence it.
|
|
136
|
+
*/
|
|
137
|
+
export async function offerUpdate(environment, pending, exitCode) {
|
|
138
|
+
if (pending === undefined)
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
const current = cliVersion();
|
|
142
|
+
const refused = exitCode === EXIT_CODES.upgrade;
|
|
143
|
+
let knowledge = await pending;
|
|
144
|
+
if (refused && !knowledge.asked) {
|
|
145
|
+
knowledge = await lookUpLatestVersion(environment, {
|
|
146
|
+
force: true,
|
|
147
|
+
timeoutMilliseconds: UPDATE_CHECK_CONFIG.BACKGROUND_TIMEOUT_MILLISECONDS
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const latest = knowledge.latest;
|
|
151
|
+
if (latest === undefined || !isNewerVersion(latest, current)) {
|
|
152
|
+
debug(environment, `no newer version than ${current} (newest known: ${latest ?? 'none'})`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!refused && isSnoozed(knowledge.record, latest, environment.now())) {
|
|
156
|
+
debug(environment, `${latest} was already offered less than ${UPDATE_CHECK_CONFIG.SNOOZE_DAYS} days ago`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await presentOffer(environment, knowledge, current, latest);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
// Whatever goes wrong here, the command has already done what it was asked to.
|
|
163
|
+
debug(environment, `the update offer failed: ${error.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { note } from "../output.js";
|
|
2
|
+
import { installVersion } from "./installation.js";
|
|
3
|
+
/** Why an update did not happen, with the command to do it by hand: what to read when it did not work. */
|
|
4
|
+
export function describeInstallFailure(result, input) {
|
|
5
|
+
const { command, packageName, latest } = input;
|
|
6
|
+
const byHand = `Update it by hand with: ${command.text}`;
|
|
7
|
+
if (result.reason === 'not-started') {
|
|
8
|
+
return `Could not run ${command.program} (is it installed and on your PATH?), so ${packageName} was not updated. ${byHand}`;
|
|
9
|
+
}
|
|
10
|
+
if (result.reason === 'unchanged') {
|
|
11
|
+
return (`${command.program} finished, but the ${packageName} installed here is still ${result.installedVersion ?? 'the same'}, ` +
|
|
12
|
+
`not ${latest}: it may have gone to another folder than the one this copy runs from. ${byHand}`);
|
|
13
|
+
}
|
|
14
|
+
return (`${command.program} stopped with exit code ${result.exitCode}, so ${packageName} was not updated. ${byHand}\n` +
|
|
15
|
+
'A permission error (EACCES) means the global folder belongs to another user: fix its ownership, or install Node with a version manager, rather than using sudo.');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs the command, checks the installed copy, and (unless `quiet`) prints what is being run and what came of it on
|
|
19
|
+
* stderr. The caller decides what a failure means for its exit code: here there is only the result.
|
|
20
|
+
*/
|
|
21
|
+
export async function performUpdate(environment, input) {
|
|
22
|
+
if (!input.quiet)
|
|
23
|
+
note(environment, `Running: ${input.command.text}`);
|
|
24
|
+
const result = await installVersion(environment, input.command, input.latest, {
|
|
25
|
+
showOutput: !input.quiet
|
|
26
|
+
});
|
|
27
|
+
if (!input.quiet) {
|
|
28
|
+
note(environment, result.ok
|
|
29
|
+
? `Updated ${input.packageName} ${input.current} → ${input.latest}. The next command you run uses the new version.`
|
|
30
|
+
: describeInstallFailure(result, input));
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { isRecord } from "../http.js";
|
|
2
|
+
import { isReleaseVersion } from "./version.js";
|
|
3
|
+
function hostOf(url) {
|
|
4
|
+
try {
|
|
5
|
+
return new URL(url).host;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return url;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** What went wrong before there was an answer, in a few words. */
|
|
12
|
+
function describeFailure(error, timeoutMilliseconds) {
|
|
13
|
+
const name = error instanceof Error ? error.name : '';
|
|
14
|
+
if (name === 'TimeoutError' || name === 'AbortError') {
|
|
15
|
+
return `no answer within ${Math.round((timeoutMilliseconds / 1000) * 10) / 10}s`;
|
|
16
|
+
}
|
|
17
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
18
|
+
return ((typeof cause?.code === 'string' && cause.code) ||
|
|
19
|
+
(typeof cause?.message === 'string' && cause.message) ||
|
|
20
|
+
(error instanceof Error ? error.message : String(error)));
|
|
21
|
+
}
|
|
22
|
+
export async function fetchLatestVersion(environment, query) {
|
|
23
|
+
const url = `${query.registryUrl.replace(/\/+$/, '')}/${query.packageName.replace('/', '%2f')}/latest`;
|
|
24
|
+
const host = hostOf(url);
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await environment.fetch(url, {
|
|
28
|
+
headers: { accept: 'application/json', 'user-agent': query.userAgent },
|
|
29
|
+
signal: AbortSignal.timeout(query.timeoutMilliseconds)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
reason: 'unreachable',
|
|
36
|
+
detail: `${host} did not answer: ${describeFailure(error, query.timeoutMilliseconds)}`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (response.status === 404) {
|
|
40
|
+
return { ok: false, reason: 'not-published', detail: `${host} has no ${query.packageName} (yet)` };
|
|
41
|
+
}
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
return { ok: false, reason: 'unreachable', detail: `${host} answered ${response.status}` };
|
|
44
|
+
}
|
|
45
|
+
let body;
|
|
46
|
+
try {
|
|
47
|
+
body = await response.json();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { ok: false, reason: 'unexpected', detail: `${host} did not answer with JSON` };
|
|
51
|
+
}
|
|
52
|
+
if (!isRecord(body) || body.name !== query.packageName || typeof body.version !== 'string') {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
reason: 'unexpected',
|
|
56
|
+
detail: `${host} did not name a version of ${query.packageName}`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (!isReleaseVersion(body.version)) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
reason: 'unexpected',
|
|
63
|
+
// Quoted as JSON, so that a control character in what the network sent is shown as an escape, not obeyed.
|
|
64
|
+
detail: `${host} names ${JSON.stringify(body.version.slice(0, 40))} as the latest, which is not a release version`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { ok: true, version: body.version };
|
|
68
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versions as npm writes them, and which of two is later. Only what looking for an update needs: the newest release
|
|
3
|
+
* (`1.2.3`) is compared with the version that is running, which may be a pre-release (`1.3.0-beta.1`).
|
|
4
|
+
*/
|
|
5
|
+
const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
6
|
+
export function parseVersion(text) {
|
|
7
|
+
const match = VERSION.exec(text.trim());
|
|
8
|
+
if (!match)
|
|
9
|
+
return undefined;
|
|
10
|
+
return {
|
|
11
|
+
major: Number(match[1]),
|
|
12
|
+
minor: Number(match[2]),
|
|
13
|
+
patch: Number(match[3]),
|
|
14
|
+
prerelease: match[4]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `1.2.3` and nothing else: three numbers, no pre-release, no build. Only such a version is ever offered, and it is the
|
|
19
|
+
* only kind that is put in a command line: digits and dots cannot be anything but a version.
|
|
20
|
+
*/
|
|
21
|
+
export function isReleaseVersion(text) {
|
|
22
|
+
return /^\d+\.\d+\.\d+$/.test(text);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Whether `candidate` is a later version than `current`. A pre-release is earlier than its own release
|
|
26
|
+
* (`1.3.0-beta.1` is before `1.3.0`), so a person on a beta is offered the release; two pre-releases of the same
|
|
27
|
+
* numbers are not told apart, and a version that cannot be read is never "later".
|
|
28
|
+
*/
|
|
29
|
+
export function isNewerVersion(candidate, current) {
|
|
30
|
+
const later = parseVersion(candidate);
|
|
31
|
+
const earlier = parseVersion(current);
|
|
32
|
+
if (!later || !earlier)
|
|
33
|
+
return false;
|
|
34
|
+
for (const part of ['major', 'minor', 'patch']) {
|
|
35
|
+
if (later[part] !== earlier[part])
|
|
36
|
+
return later[part] > earlier[part];
|
|
37
|
+
}
|
|
38
|
+
return earlier.prerelease !== undefined && later.prerelease === undefined;
|
|
39
|
+
}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { validationError } from "./errors.js";
|
|
2
|
+
import { patternRequirement } from "./patterns.js";
|
|
3
|
+
let ajvPromise;
|
|
4
|
+
const compiled = new WeakMap();
|
|
5
|
+
async function compilerFor() {
|
|
6
|
+
ajvPromise ??= import('ajv').then(({ Ajv }) => new Ajv({ strict: false, validateFormats: false, allErrors: true }));
|
|
7
|
+
return ajvPromise;
|
|
8
|
+
}
|
|
9
|
+
/** `/files/0/url` becomes `files[0].url`. */
|
|
10
|
+
function dotted(pointer) {
|
|
11
|
+
return pointer
|
|
12
|
+
.split('/')
|
|
13
|
+
.filter((segment) => segment !== '')
|
|
14
|
+
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
15
|
+
.reduce((path, segment) => /^\d+$/.test(segment) ? `${path}[${segment}]` : path === '' ? segment : `${path}.${segment}`, '');
|
|
16
|
+
}
|
|
17
|
+
function issueOf(error) {
|
|
18
|
+
const path = dotted(error.instancePath);
|
|
19
|
+
const join = (child) => (path === '' ? child : `${path}.${child}`);
|
|
20
|
+
switch (error.keyword) {
|
|
21
|
+
case 'required':
|
|
22
|
+
return {
|
|
23
|
+
path: join(String(error.params.missingProperty)),
|
|
24
|
+
message: 'is required'
|
|
25
|
+
};
|
|
26
|
+
case 'additionalProperties':
|
|
27
|
+
return {
|
|
28
|
+
path: join(String(error.params.additionalProperty)),
|
|
29
|
+
message: 'is not accepted here'
|
|
30
|
+
};
|
|
31
|
+
case 'enum':
|
|
32
|
+
return {
|
|
33
|
+
path,
|
|
34
|
+
message: `must be one of: ${(error.params.allowedValues ?? []).map(String).join(', ')}`
|
|
35
|
+
};
|
|
36
|
+
case 'pattern':
|
|
37
|
+
return { path, message: `must ${patternRequirement(error.params.pattern)}` };
|
|
38
|
+
case 'anyOf':
|
|
39
|
+
case 'oneOf':
|
|
40
|
+
return { path, message: 'does not match any of the shapes the contract allows' };
|
|
41
|
+
default:
|
|
42
|
+
return { path, message: error.message ?? 'is not valid' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The things wrong with `value`, or an empty list. */
|
|
46
|
+
export async function checkAgainst(schema, value) {
|
|
47
|
+
let validate = compiled.get(schema);
|
|
48
|
+
if (!validate) {
|
|
49
|
+
validate = (await compilerFor()).compile(schema);
|
|
50
|
+
compiled.set(schema, validate);
|
|
51
|
+
}
|
|
52
|
+
if (validate(value))
|
|
53
|
+
return [];
|
|
54
|
+
// The same problem is often reported twice (a missing field and the alternative that needed it): say each once.
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
const issues = [];
|
|
57
|
+
for (const error of validate.errors ?? []) {
|
|
58
|
+
const issue = issueOf(error);
|
|
59
|
+
const key = `${issue.path}|${issue.message}`;
|
|
60
|
+
if (!seen.has(key)) {
|
|
61
|
+
seen.add(key);
|
|
62
|
+
issues.push(issue);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return issues;
|
|
66
|
+
}
|
|
67
|
+
const MOST_ISSUES_SHOWN = 10;
|
|
68
|
+
/**
|
|
69
|
+
* A place in a document named the way the person typed it: `title` is `--title`, and a place inside a flag's value
|
|
70
|
+
* (`media_ids[0]`) is `--media-ids[0]`. A place that no flag fills (`files[0].url`) stays as the document spells it.
|
|
71
|
+
*/
|
|
72
|
+
export function namedByFlag(path, flagOf) {
|
|
73
|
+
const property = /^[^.[]+/.exec(path)?.[0];
|
|
74
|
+
if (property === undefined)
|
|
75
|
+
return path;
|
|
76
|
+
const flag = flagOf.get(property);
|
|
77
|
+
return flag === undefined ? path : `--${flag}${path.slice(property.length)}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Stops the command when the document does not fit its schema. `nameOf` turns a place in the document into what the
|
|
81
|
+
* person typed (`--title` for the property `title`), so the message points at a flag whenever there is one.
|
|
82
|
+
*/
|
|
83
|
+
export async function requireValid(schema, value, what, nameOf = (path) => path) {
|
|
84
|
+
const issues = await checkAgainst(schema, value);
|
|
85
|
+
if (issues.length === 0)
|
|
86
|
+
return;
|
|
87
|
+
const shown = issues
|
|
88
|
+
.slice(0, MOST_ISSUES_SHOWN)
|
|
89
|
+
.map((issue) => ` ${issue.path === '' ? what : nameOf(issue.path)} ${issue.message}`);
|
|
90
|
+
if (issues.length > MOST_ISSUES_SHOWN)
|
|
91
|
+
shown.push(` … and ${issues.length - MOST_ISSUES_SHOWN} more`);
|
|
92
|
+
throw validationError(`${what} is not valid:\n${shown.join('\n')}`, issues.map((issue) => ({ path: issue.path, message: issue.message })), 'Nothing was sent. `widgetic schema` prints what the command takes.');
|
|
93
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
let cached;
|
|
3
|
+
function readManifest() {
|
|
4
|
+
if (cached)
|
|
5
|
+
return cached;
|
|
6
|
+
const text = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
|
|
7
|
+
const parsed = JSON.parse(text);
|
|
8
|
+
if (typeof parsed.name !== 'string' || typeof parsed.version !== 'string') {
|
|
9
|
+
throw new Error('package.json of @widgetic/cli has no name or version');
|
|
10
|
+
}
|
|
11
|
+
cached = {
|
|
12
|
+
name: parsed.name,
|
|
13
|
+
version: parsed.version,
|
|
14
|
+
...(parsed.engines ? { engines: parsed.engines } : {})
|
|
15
|
+
};
|
|
16
|
+
return cached;
|
|
17
|
+
}
|
|
18
|
+
export function cliPackageName() {
|
|
19
|
+
return readManifest().name;
|
|
20
|
+
}
|
|
21
|
+
export function cliVersion() {
|
|
22
|
+
return readManifest().version;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* How the CLI names itself to the gateway: `@widgetic/cli/<version>`. The gateway's SDK version gate reads this from
|
|
26
|
+
* `X-SDK-Version` and from the `User-Agent`, counts it, and can answer 426 to a release it has retired.
|
|
27
|
+
*/
|
|
28
|
+
export function cliIdentity() {
|
|
29
|
+
const manifest = readManifest();
|
|
30
|
+
return `${manifest.name}/${manifest.version}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The two headers by which the gateway recognises this CLI: its SDK version gate reads `X-SDK-Version` (and the user
|
|
34
|
+
* agent) to count the release and, one day, to refuse one it has retired. Every request the CLI makes carries them.
|
|
35
|
+
*/
|
|
36
|
+
export function identityHeaders(environment) {
|
|
37
|
+
return {
|
|
38
|
+
'user-agent': `${cliIdentity()} (node/${environment.nodeVersion}; ${environment.platform})`,
|
|
39
|
+
'x-sdk-version': cliIdentity()
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** The oldest Node this package runs on, from `engines.node` (`>=22.12.0` is `22.12.0`); `undefined` when it says nothing usable. */
|
|
43
|
+
export function minimumNodeVersion() {
|
|
44
|
+
const range = readManifest().engines?.node;
|
|
45
|
+
const match = typeof range === 'string' ? /^>=\s*(\d+(?:\.\d+){0,2})$/.exec(range.trim()) : null;
|
|
46
|
+
return match?.[1];
|
|
47
|
+
}
|
|
48
|
+
/** Whether `version` (`26.12.1`) is `minimum` (`22.12.0`) or later; a part that is missing counts as 0. */
|
|
49
|
+
export function isAtLeast(version, minimum) {
|
|
50
|
+
const parse = (text) => text.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
51
|
+
const have = parse(version);
|
|
52
|
+
const need = parse(minimum);
|
|
53
|
+
for (let index = 0; index < Math.max(have.length, need.length); index += 1) {
|
|
54
|
+
const difference = (have[index] ?? 0) - (need[index] ?? 0);
|
|
55
|
+
if (difference !== 0)
|
|
56
|
+
return difference > 0;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"package":"@widgetic/api-contracts","version":"1.11.109","publishTag":"latest","source":{"commit":"21238b5bbb97b100ef2edb0d9d98d167467e8d18","branch":null,"dirty":false,"atOriginMain":true},"openApi":{"file":"openapi-public.json","sha256":"750e774e958a819377963d0a6f7a2feb2e7b70327b614053fdbe748e3e5e5865","paths":120,"operations":161,"servers":["https://api.widgetic.com/v1"]},"mcp":{"file":"mcp-tools.json","sha256":"9d6494d8d1d227e1752a170339d63853b52770812e9283cda2ca66981edd72c6","contractVersion":"1.12.0","serverName":"widgetic","tools":32}}
|