@vatio-ai/cli 0.49.0 → 0.50.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/commands/workspace-admin.mjs +8 -5
- package/lib/help.mjs +3 -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) {
|
|
@@ -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
|
@@ -51,7 +51,8 @@ flag, the directory decides.
|
|
|
51
51
|
kb unfollow BASE URL | refresh BASE [URL]
|
|
52
52
|
|
|
53
53
|
Credentials and the widget:
|
|
54
|
-
secrets list|set KEY VALUE|rm KEY
|
|
54
|
+
secrets list|set KEY [VALUE]|rm KEY
|
|
55
|
+
Without VALUE, set asks for it with the input hidden
|
|
55
56
|
tokens list|create [--env NAME] [--label NAME]|revoke PREFIX
|
|
56
57
|
widget [--env NAME] What the platform enforces, and the tokens there are
|
|
57
58
|
auth --new-key Keypair that signs the JWT saying who a visitor is
|
|
@@ -69,7 +70,7 @@ flag, the directory decides.
|
|
|
69
70
|
config show|get KEY|set KEY VALUE|unset KEY
|
|
70
71
|
Keys: base_url, token
|
|
71
72
|
|
|
72
|
-
Environment: VATIO_BASE_URL, VATIO_TOKEN, VATIO_HOME
|
|
73
|
+
Environment: VATIO_BASE_URL, VATIO_TOKEN, VATIO_HOME, VATIO_NO_UPDATE_NOTIFIER
|
|
73
74
|
|
|
74
75
|
Docs: https://docs.vatio.ai
|
|
75
76
|
`;
|
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
|
+
}
|