@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 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
- const value = args.shift();
50
- if (!key || value === undefined) fail("Usage: vatio secrets set KEY VALUE");
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(`Set ${key} on workspace ${workspace}`);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vatio-ai/cli",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Vatio CLI — deploy and manage Vatio agent workspaces",
5
5
  "keywords": [
6
6
  "vatio",