@vatio-ai/cli 0.49.0 → 0.51.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/bin/vatio.mjs +7 -0
- package/lib/api-client.mjs +18 -0
- package/lib/commands/knowledge.mjs +89 -5
- package/lib/commands/mcp.mjs +6 -2
- package/lib/commands/workspace-admin.mjs +8 -5
- package/lib/help.mjs +6 -2
- package/lib/support.mjs +56 -0
- package/lib/update-notice.mjs +99 -0
- package/package.json +1 -1
package/bin/vatio.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { kb } from "../lib/commands/knowledge.mjs";
|
|
|
24
24
|
import { instagram, whatsapp } from "../lib/commands/channels.mjs";
|
|
25
25
|
import { docs, issue } from "../lib/commands/support-commands.mjs";
|
|
26
26
|
import { mcp } from "../lib/commands/mcp.mjs";
|
|
27
|
+
import { printUpdateNotice, startUpdateCheck } from "../lib/update-notice.mjs";
|
|
27
28
|
|
|
28
29
|
// Two commands that existed and are gone, answered by name rather than as
|
|
29
30
|
// unknown: one was removed from the product, the other is what npm does.
|
|
@@ -38,6 +39,12 @@ const RETIRED = {
|
|
|
38
39
|
async function main(argv) {
|
|
39
40
|
const args = argv.slice(2);
|
|
40
41
|
const command = args.shift();
|
|
42
|
+
const updateCheck = startUpdateCheck(command);
|
|
43
|
+
await run(command, args);
|
|
44
|
+
await printUpdateNotice(updateCheck);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function run(command, args) {
|
|
41
48
|
const config = new Config({ startDir: process.cwd() });
|
|
42
49
|
|
|
43
50
|
switch (command) {
|
package/lib/api-client.mjs
CHANGED
|
@@ -127,6 +127,24 @@ export class ApiClient extends HttpClient {
|
|
|
127
127
|
return this.post(`/knowledge_bases/${encode(base)}/sites/refresh`, { url });
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// Writes land in the base's draft, which preview answers from; live moves
|
|
131
|
+
// only on publish or rollback.
|
|
132
|
+
knowledgeChanges(base) {
|
|
133
|
+
return this.get(`/knowledge_bases/${encode(base)}/changes`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
publishKnowledgeBase({ base, note }) {
|
|
137
|
+
return this.post(`/knowledge_bases/${encode(base)}/publish`, { note });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
knowledgeSnapshots(base) {
|
|
141
|
+
return this.get(`/knowledge_bases/${encode(base)}/snapshots`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
rollbackKnowledgeBase({ base, number }) {
|
|
145
|
+
return this.post(`/knowledge_bases/${encode(base)}/snapshots/${encode(String(number))}/rollback`, {});
|
|
146
|
+
}
|
|
147
|
+
|
|
130
148
|
// --- publishable tokens and the widget ------------------------------
|
|
131
149
|
|
|
132
150
|
listPublishableTokens() {
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A base belongs to the workspace, not to a deployment: `knowledge:` in
|
|
4
4
|
// vatio.yml is a list of *names*, and a push neither fills nor empties one.
|
|
5
|
-
// That is why this is a command and not part of the manifest
|
|
6
|
-
//
|
|
5
|
+
// That is why this is a command and not part of the manifest.
|
|
6
|
+
//
|
|
7
|
+
// Every write goes to the base's draft, which preview answers from. Live
|
|
8
|
+
// answers from the last published snapshot and changes only on `publish` --
|
|
9
|
+
// or `rollback`, which points it at an earlier one. So: write, try it on
|
|
10
|
+
// preview, publish.
|
|
7
11
|
//
|
|
8
12
|
// A base holds entries, and an entry is markdown. Either you wrote it, or a
|
|
9
13
|
// site wrote it from a page it read — and a site is one URL or one pattern,
|
|
@@ -30,6 +34,10 @@ export async function kb(config, args) {
|
|
|
30
34
|
case "follow": return await follow(client, workspace, args.shift(), args.shift());
|
|
31
35
|
case "unfollow": return await unfollow(client, workspace, args.shift(), args.shift());
|
|
32
36
|
case "refresh": return await refresh(client, workspace, args.shift(), args.shift());
|
|
37
|
+
case "status": return await status(client, workspace, args.shift());
|
|
38
|
+
case "publish": return await publish(client, workspace, args);
|
|
39
|
+
case "history": return await history(client, workspace, args.shift());
|
|
40
|
+
case "rollback": return await rollback(client, workspace, args.shift(), args.shift());
|
|
33
41
|
default: fail(`Unknown kb subcommand: ${sub}\n\n${usage()}`);
|
|
34
42
|
}
|
|
35
43
|
}
|
|
@@ -45,7 +53,11 @@ function usage() {
|
|
|
45
53
|
vatio kb rm-entry BASE ENTRY Delete one entry
|
|
46
54
|
vatio kb follow BASE URL Read a site into the base, nightly
|
|
47
55
|
vatio kb unfollow BASE URL Stop reading it, and drop its entries
|
|
48
|
-
vatio kb refresh BASE [URL] Read the sites again now
|
|
56
|
+
vatio kb refresh BASE [URL] Read the sites again now
|
|
57
|
+
vatio kb status BASE What the draft has that live does not
|
|
58
|
+
vatio kb publish BASE [-m NOTE] Take the draft live
|
|
59
|
+
vatio kb history BASE Every publication, newest first
|
|
60
|
+
vatio kb rollback BASE NUMBER Point live back at publication NUMBER`;
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
async function list(client, workspace) {
|
|
@@ -80,6 +92,7 @@ async function show(client, workspace, name) {
|
|
|
80
92
|
`${base.name} on workspace ${workspace}: entries=${base.entries_count} ` +
|
|
81
93
|
`ready=${base.answerable_count}${referenceNote(base)}`
|
|
82
94
|
);
|
|
95
|
+
console.log(` ${liveNote(base)}`);
|
|
83
96
|
|
|
84
97
|
const sites = asArray(base.sites);
|
|
85
98
|
if (sites.length > 0) {
|
|
@@ -106,10 +119,20 @@ async function show(client, workspace, name) {
|
|
|
106
119
|
console.log("\n entries:");
|
|
107
120
|
for (const entry of entries) {
|
|
108
121
|
const origin = entry.written ? "written here" : entry.url;
|
|
109
|
-
|
|
122
|
+
const version = entry.version ? ` v${entry.version}` : "";
|
|
123
|
+
console.log(` ${entry.name}${version} ${entry.state ?? "?"} ${origin}`);
|
|
110
124
|
}
|
|
111
125
|
}
|
|
112
126
|
|
|
127
|
+
// Where live stands, and whether the draft has moved past it.
|
|
128
|
+
function liveNote(base) {
|
|
129
|
+
const live = base.live_snapshot ? `live=publication #${base.live_snapshot}` : "live=never published";
|
|
130
|
+
const pending = base.pending_changes
|
|
131
|
+
? ` ${base.pending_changes} unpublished change${base.pending_changes === 1 ? "" : "s"} — \`vatio kb publish ${base.name}\``
|
|
132
|
+
: "";
|
|
133
|
+
return `${live}${pending}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
113
136
|
// Several names, because the moment this command is most often run is a push
|
|
114
137
|
// that just refused for naming bases that do not exist yet -- and that push
|
|
115
138
|
// names all of them at once. One base per invocation turned a first deploy
|
|
@@ -165,7 +188,9 @@ async function write(client, workspace, args) {
|
|
|
165
188
|
}
|
|
166
189
|
|
|
167
190
|
const row = await client.writeKnowledgeEntry({ base, name, content });
|
|
168
|
-
|
|
191
|
+
const version = row.version ? ` (v${row.version})` : "";
|
|
192
|
+
console.log(`Wrote ${row.name}${version} to the draft of ${base} on workspace ${workspace}`);
|
|
193
|
+
console.log(`Preview answers with it now; \`vatio kb publish ${base}\` takes it live.`);
|
|
169
194
|
}
|
|
170
195
|
|
|
171
196
|
// Prints exactly what the platform holds, so a diff against the file you
|
|
@@ -221,6 +246,65 @@ async function refresh(client, workspace, base, url) {
|
|
|
221
246
|
console.log(`Runs in the background — check progress with \`vatio kb show ${base}\``);
|
|
222
247
|
}
|
|
223
248
|
|
|
249
|
+
// The draft against live, entry by entry -- what `publish` would do.
|
|
250
|
+
async function status(client, workspace, base) {
|
|
251
|
+
if (!base) fail("usage: vatio kb status BASE");
|
|
252
|
+
|
|
253
|
+
const changes = asArray((await client.knowledgeChanges(base)).data);
|
|
254
|
+
if (changes.length === 0) {
|
|
255
|
+
console.log(`${base} on workspace ${workspace}: everything is published`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log(`${base} on workspace ${workspace}: ${changes.length} unpublished change${changes.length === 1 ? "" : "s"}`);
|
|
260
|
+
for (const change of changes) {
|
|
261
|
+
const from = change.live_version ? `v${change.live_version}` : "—";
|
|
262
|
+
const to = change.draft_version ? `v${change.draft_version}` : "—";
|
|
263
|
+
console.log(` ${change.change.padEnd(8)} ${change.name} ${from} → ${to}`);
|
|
264
|
+
}
|
|
265
|
+
console.log(`\nPublish with \`vatio kb publish ${base} -m "what changed"\``);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function publish(client, workspace, args) {
|
|
269
|
+
const base = args.shift();
|
|
270
|
+
if (!base) fail("usage: vatio kb publish BASE [-m NOTE]");
|
|
271
|
+
|
|
272
|
+
let note = "";
|
|
273
|
+
const flag = args.findIndex((arg) => arg === "-m" || arg === "--note");
|
|
274
|
+
if (flag !== -1) note = args[flag + 1] ?? "";
|
|
275
|
+
|
|
276
|
+
const snapshot = await client.publishKnowledgeBase({ base, note });
|
|
277
|
+
console.log(`Published ${base} on workspace ${workspace} as #${snapshot.number} (${snapshot.entries} entries)`);
|
|
278
|
+
console.log(`Live answers from it now. Undo with \`vatio kb rollback ${base} NUMBER\` — see \`vatio kb history ${base}\`.`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function history(client, workspace, base) {
|
|
282
|
+
if (!base) fail("usage: vatio kb history BASE");
|
|
283
|
+
|
|
284
|
+
const snapshots = asArray((await client.knowledgeSnapshots(base)).data);
|
|
285
|
+
if (snapshots.length === 0) {
|
|
286
|
+
console.log(`${base} on workspace ${workspace} was never published — \`vatio kb publish ${base}\``);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log(`Publications of ${base} on workspace ${workspace}:`);
|
|
291
|
+
for (const snapshot of snapshots) {
|
|
292
|
+
const live = snapshot.live ? " ← live" : "";
|
|
293
|
+
const who = snapshot.author ? ` ${snapshot.author}` : "";
|
|
294
|
+
const note = snapshot.note ? ` ${snapshot.note}` : "";
|
|
295
|
+
console.log(` #${snapshot.number} ${snapshot.created_at}${who} entries=${snapshot.entries}${note}${live}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Only the pointer moves: the draft, and every later publication, stay.
|
|
300
|
+
async function rollback(client, workspace, base, number) {
|
|
301
|
+
if (!base || !number || !/^\d+$/.test(number)) fail("usage: vatio kb rollback BASE NUMBER");
|
|
302
|
+
|
|
303
|
+
await client.rollbackKnowledgeBase({ base, number });
|
|
304
|
+
console.log(`Live for ${base} on workspace ${workspace} is publication #${number} again`);
|
|
305
|
+
console.log("The draft is untouched; publish again whenever it is ready.");
|
|
306
|
+
}
|
|
307
|
+
|
|
224
308
|
// Blocking on purpose: `vatio kb write docs refunds < refunds.md` should behave
|
|
225
309
|
// like every other filter, and a terminal with nobody piping into it returns ""
|
|
226
310
|
// rather than hanging on a read that will never end.
|
package/lib/commands/mcp.mjs
CHANGED
|
@@ -94,10 +94,14 @@ const COMMANDS = {
|
|
|
94
94
|
annotations: { readOnlyHint: false }
|
|
95
95
|
},
|
|
96
96
|
kb: {
|
|
97
|
-
usage:
|
|
97
|
+
usage:
|
|
98
|
+
"[list] | show NAME | create NAME | write BASE ENTRY [FILE] | cat BASE ENTRY | rm-entry BASE ENTRY | " +
|
|
99
|
+
"follow BASE URL | unfollow BASE URL | refresh BASE [URL] | status BASE | publish BASE [-m NOTE] | " +
|
|
100
|
+
"history BASE | rollback BASE NUMBER",
|
|
98
101
|
summary:
|
|
99
102
|
"Knowledge bases, which belong to the workspace rather than to a deployment: `knowledge:` in " +
|
|
100
|
-
"vatio.yml is a list of names, and a push neither fills nor empties one."
|
|
103
|
+
"vatio.yml is a list of names, and a push neither fills nor empties one. Writes go to a draft that " +
|
|
104
|
+
"preview answers from; live changes only on publish.",
|
|
101
105
|
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
102
106
|
},
|
|
103
107
|
tokens: {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// vatio.yml and all of them are here.
|
|
8
8
|
|
|
9
9
|
import { deployClient } from "./deploy.mjs";
|
|
10
|
-
import { fail, takeValue } from "../support.mjs";
|
|
10
|
+
import { askSecret, fail, paint, takeValue } from "../support.mjs";
|
|
11
11
|
|
|
12
12
|
// live, preview, or a named preview like pr-42. This used to accept only the
|
|
13
13
|
// first two, which meant a workspace could deploy to `pr-42` and then have no
|
|
@@ -46,10 +46,13 @@ export async function secrets(config, args) {
|
|
|
46
46
|
}
|
|
47
47
|
if (sub === "set") {
|
|
48
48
|
const key = args.shift();
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
if (!key) fail("Usage: vatio secrets set KEY [VALUE]");
|
|
50
|
+
// Asked for rather than typed on the command line, where it would stay in
|
|
51
|
+
// shell history.
|
|
52
|
+
const value = args.shift() ?? (await askSecret(`${paint("green", "?")} Paste your secret: `));
|
|
53
|
+
if (value === "") fail(`No value given for ${key}`);
|
|
51
54
|
await client.upsertSecret({ key, value });
|
|
52
|
-
console.log(
|
|
55
|
+
console.log(`${paint("green", "✓")} Set secret ${key} on workspace ${workspace}`);
|
|
53
56
|
return;
|
|
54
57
|
}
|
|
55
58
|
if (sub === "rm" || sub === "remove" || sub === "delete") {
|
|
@@ -60,7 +63,7 @@ export async function secrets(config, args) {
|
|
|
60
63
|
return;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
fail("Usage: vatio secrets list|set KEY VALUE|rm KEY");
|
|
66
|
+
fail("Usage: vatio secrets list|set KEY [VALUE]|rm KEY");
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
export async function tokens(config, args) {
|
package/lib/help.mjs
CHANGED
|
@@ -49,9 +49,13 @@ flag, the directory decides.
|
|
|
49
49
|
kb rm-entry BASE ENTRY Delete one entry
|
|
50
50
|
kb follow BASE URL Read a site into the base, nightly
|
|
51
51
|
kb unfollow BASE URL | refresh BASE [URL]
|
|
52
|
+
kb status BASE What the draft has that live does not
|
|
53
|
+
kb publish BASE [-m NOTE] Take the draft live (preview already has it)
|
|
54
|
+
kb history BASE | rollback BASE NUMBER
|
|
52
55
|
|
|
53
56
|
Credentials and the widget:
|
|
54
|
-
secrets list|set KEY VALUE|rm KEY
|
|
57
|
+
secrets list|set KEY [VALUE]|rm KEY
|
|
58
|
+
Without VALUE, set asks for it with the input hidden
|
|
55
59
|
tokens list|create [--env NAME] [--label NAME]|revoke PREFIX
|
|
56
60
|
widget [--env NAME] What the platform enforces, and the tokens there are
|
|
57
61
|
auth --new-key Keypair that signs the JWT saying who a visitor is
|
|
@@ -69,7 +73,7 @@ flag, the directory decides.
|
|
|
69
73
|
config show|get KEY|set KEY VALUE|unset KEY
|
|
70
74
|
Keys: base_url, token
|
|
71
75
|
|
|
72
|
-
Environment: VATIO_BASE_URL, VATIO_TOKEN, VATIO_HOME
|
|
76
|
+
Environment: VATIO_BASE_URL, VATIO_TOKEN, VATIO_HOME, VATIO_NO_UPDATE_NOTIFIER
|
|
73
77
|
|
|
74
78
|
Docs: https://docs.vatio.ai
|
|
75
79
|
`;
|
package/lib/support.mjs
CHANGED
|
@@ -118,3 +118,59 @@ export async function ask(question) {
|
|
|
118
118
|
rl.close();
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
const COLORS = { green: 32, yellow: 33, cyan: 36 };
|
|
123
|
+
|
|
124
|
+
export function paint(color, text) {
|
|
125
|
+
if (process.env.NO_COLOR || !process.stderr.isTTY) return text;
|
|
126
|
+
return `\x1b[${COLORS[color]}m${text}\x1b[0m`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A value typed or pasted with a star echoed per character, gh-style, so a
|
|
130
|
+
// secret never lands in shell history or terminal scrollback. Off a terminal
|
|
131
|
+
// the value is whatever arrives on stdin: `op read … | vatio secrets set KEY`.
|
|
132
|
+
export async function askSecret(question) {
|
|
133
|
+
if (!process.stdin.isTTY) return (await readStdin()).replace(/\r?\n$/, "");
|
|
134
|
+
|
|
135
|
+
const input = process.stdin;
|
|
136
|
+
const output = process.stderr;
|
|
137
|
+
output.write(question);
|
|
138
|
+
input.setRawMode(true);
|
|
139
|
+
input.setEncoding("utf8");
|
|
140
|
+
input.resume();
|
|
141
|
+
|
|
142
|
+
return await new Promise((resolve, reject) => {
|
|
143
|
+
let value = "";
|
|
144
|
+
const finish = (error) => {
|
|
145
|
+
input.removeListener("data", onData);
|
|
146
|
+
input.setRawMode(false);
|
|
147
|
+
input.pause();
|
|
148
|
+
output.write("\n");
|
|
149
|
+
if (error) reject(error);
|
|
150
|
+
else resolve(value);
|
|
151
|
+
};
|
|
152
|
+
const onData = (chunk) => {
|
|
153
|
+
for (const char of chunk) {
|
|
154
|
+
if (char === "\r" || char === "\n") return finish();
|
|
155
|
+
if (char === "\u0003") return finish(new UserError("Cancelled"));
|
|
156
|
+
if (char === "\u007f" || char === "\b") {
|
|
157
|
+
if (value.length > 0) {
|
|
158
|
+
value = [...value].slice(0, -1).join("");
|
|
159
|
+
output.write("\b \b");
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (char < " ") continue;
|
|
164
|
+
value += char;
|
|
165
|
+
output.write("*");
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
input.on("data", onData);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function readStdin() {
|
|
173
|
+
const chunks = [];
|
|
174
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
175
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
176
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// "A new release of vatio is available", after a command, the way gh does it.
|
|
2
|
+
//
|
|
3
|
+
// The registry is asked at most once a day and the answer is kept in
|
|
4
|
+
// ~/.vatio/update-check.json, so only the run that refreshes it makes a
|
|
5
|
+
// request -- and that request runs alongside the command, not before it.
|
|
6
|
+
// Nothing prints unless stderr is a terminal: a pipe, CI, or `vatio mcp`
|
|
7
|
+
// (whose children write to a pipe) never sees it.
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { configHome } from "./config.mjs";
|
|
13
|
+
import { VERSION } from "./version.mjs";
|
|
14
|
+
import { paint } from "./support.mjs";
|
|
15
|
+
|
|
16
|
+
const PACKAGE = "@vatio-ai/cli";
|
|
17
|
+
const LATEST_URL = `https://registry.npmjs.org/${PACKAGE}/latest`;
|
|
18
|
+
const CHECK_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
const FETCH_TIMEOUT_MS = 1_500;
|
|
20
|
+
const SKIPPED_COMMANDS = ["mcp", "version", "--version", "-v"];
|
|
21
|
+
|
|
22
|
+
export function startUpdateCheck(command) {
|
|
23
|
+
if (!shouldCheck(command)) return null;
|
|
24
|
+
|
|
25
|
+
const path = join(configHome(), "update-check.json");
|
|
26
|
+
const cached = readState(path);
|
|
27
|
+
if (cached && Date.now() - Date.parse(cached.checked_at) < CHECK_EVERY_MS) {
|
|
28
|
+
return Promise.resolve(cached.latest);
|
|
29
|
+
}
|
|
30
|
+
return fetchLatest().then((latest) => {
|
|
31
|
+
if (latest) writeState(path, { checked_at: new Date().toISOString(), latest });
|
|
32
|
+
return latest ?? cached?.latest ?? null;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function printUpdateNotice(check) {
|
|
37
|
+
if (!check) return;
|
|
38
|
+
const latest = await check.catch(() => null);
|
|
39
|
+
if (!latest || !isNewer(latest, VERSION)) return;
|
|
40
|
+
|
|
41
|
+
// npx sets npm_command=exec; a global install has no npm around it at all.
|
|
42
|
+
const upgrade = process.env.npm_command === "exec"
|
|
43
|
+
? `npx ${PACKAGE}@latest`
|
|
44
|
+
: `npm install -g ${PACKAGE}@latest`;
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`\n${paint("yellow", `A new release of vatio is available: ${VERSION} → ${latest}`)}\n` +
|
|
47
|
+
`To upgrade, run: ${upgrade}\n` +
|
|
48
|
+
`${paint("cyan", "https://docs.vatio.ai/changelog")}\n`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shouldCheck(command) {
|
|
53
|
+
if (SKIPPED_COMMANDS.includes(command)) return false;
|
|
54
|
+
if (!process.stderr.isTTY) return false;
|
|
55
|
+
if (process.env.CI || process.env.VATIO_NO_UPDATE_NOTIFIER) return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchLatest() {
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(LATEST_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
62
|
+
if (!response.ok) return null;
|
|
63
|
+
const version = (await response.json())?.version;
|
|
64
|
+
return typeof version === "string" ? version : null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readState(path) {
|
|
71
|
+
if (!existsSync(path)) return null;
|
|
72
|
+
try {
|
|
73
|
+
const state = JSON.parse(readFileSync(path, "utf8"));
|
|
74
|
+
return typeof state?.latest === "string" && typeof state?.checked_at === "string" ? state : null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeState(path, state) {
|
|
81
|
+
try {
|
|
82
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
83
|
+
writeFileSync(path, `${JSON.stringify(state)}\n`);
|
|
84
|
+
} catch {
|
|
85
|
+
// A read-only home only means the next run asks the registry again.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Plain x.y.z; a prerelease on either side is never announced.
|
|
90
|
+
function isNewer(candidate, current) {
|
|
91
|
+
const parse = (value) => /^(\d+)\.(\d+)\.(\d+)$/.exec(value)?.slice(1).map(Number);
|
|
92
|
+
const a = parse(candidate);
|
|
93
|
+
const b = parse(current);
|
|
94
|
+
if (!a || !b) return false;
|
|
95
|
+
for (let i = 0; i < 3; i += 1) {
|
|
96
|
+
if (a[i] !== b[i]) return a[i] > b[i];
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|