@vatio-ai/cli 0.37.2
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.txt +16 -0
- package/README.md +61 -0
- package/bin/vatio.mjs +132 -0
- package/lib/api-client.mjs +286 -0
- package/lib/bundle.mjs +90 -0
- package/lib/commands/auth.mjs +64 -0
- package/lib/commands/channels.mjs +322 -0
- package/lib/commands/chat.mjs +242 -0
- package/lib/commands/deploy.mjs +237 -0
- package/lib/commands/knowledge.mjs +256 -0
- package/lib/commands/mcp.mjs +307 -0
- package/lib/commands/misc.mjs +148 -0
- package/lib/commands/support-commands.mjs +147 -0
- package/lib/commands/workspace-admin.mjs +128 -0
- package/lib/config.mjs +223 -0
- package/lib/device-auth.mjs +68 -0
- package/lib/help.mjs +69 -0
- package/lib/http.mjs +151 -0
- package/lib/support.mjs +106 -0
- package/lib/version.mjs +11 -0
- package/lib/workspace.mjs +75 -0
- package/package.json +34 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Copyright (c) 2026 Urcalab. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software (the "Vatio CLI") is distributed solely for the purpose
|
|
4
|
+
of installing, running, and interacting with the Vatio platform at
|
|
5
|
+
https://vatio.ai and its APIs.
|
|
6
|
+
|
|
7
|
+
Permission is granted to install and use the Vatio CLI as distributed,
|
|
8
|
+
for that purpose. Redistribution, modification, reverse engineering,
|
|
9
|
+
sublicensing, or any use of this software independent of the Vatio
|
|
10
|
+
platform is not permitted without prior written permission from
|
|
11
|
+
Urcalab.
|
|
12
|
+
|
|
13
|
+
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
|
|
16
|
+
NONINFRINGEMENT.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @vatio-ai/cli
|
|
2
|
+
|
|
3
|
+
The CLI for [Vatio](https://vatio.ai) — deploy and manage AI agent workspaces.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @vatio-ai/cli init my-agent
|
|
7
|
+
npx @vatio-ai/cli push
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
No runtime to install first: if you have Node, you have this.
|
|
11
|
+
|
|
12
|
+
The package is `@vatio-ai/cli`; the command it installs is `vatio`. So
|
|
13
|
+
`npm install -g @vatio-ai/cli` gives you `vatio push`, and `npx @vatio-ai/cli push`
|
|
14
|
+
is the same thing without installing anything.
|
|
15
|
+
|
|
16
|
+
## What a workspace is
|
|
17
|
+
|
|
18
|
+
A directory with a `vatio.yml` in it. That file says which Vatio workspace the
|
|
19
|
+
directory deploys to, what the agent does, and how the widget looks; `tools/`,
|
|
20
|
+
`lib/` and `auth/` next to it are plain JavaScript the agent runs.
|
|
21
|
+
|
|
22
|
+
Nothing constrains where that directory lives — an agent can sit inside the
|
|
23
|
+
repository of the backend it calls, and every command except `init` finds it by
|
|
24
|
+
walking up from the current directory. There is no `--workspace` flag.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
init [SLUG] [--name NAME] Create vatio.yml here, and the remote to match
|
|
30
|
+
login [--base-url URL] Authorize this machine in a browser
|
|
31
|
+
push [--env NAME] Validate the workspace, then update a preview
|
|
32
|
+
publish [--env NAME] Promote a preview to live
|
|
33
|
+
diff [--env NAME] What this directory would change
|
|
34
|
+
status Preview and live deployment state
|
|
35
|
+
rollback Restore the previous live deployment
|
|
36
|
+
tools check Validate the workspace against Vatio's contracts
|
|
37
|
+
secrets list|set|rm Credentials your tools read as ctx.env
|
|
38
|
+
tokens list|create|revoke Publishable tokens for the widget and the SDK
|
|
39
|
+
widget [--env NAME] What the platform enforces, and the tokens there are
|
|
40
|
+
config show|set|unset base_url and token, per developer
|
|
41
|
+
doctor Node, config, workspace and token status
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`--env NAME` targets a deployment: `live`, `preview`, or a named preview like
|
|
45
|
+
`pr-42`.
|
|
46
|
+
|
|
47
|
+
Run `npx @vatio-ai/cli help` for the whole surface, and see
|
|
48
|
+
[vatio.ai/docs](https://vatio.ai/docs) for the platform.
|
|
49
|
+
|
|
50
|
+
## Where your credentials live
|
|
51
|
+
|
|
52
|
+
`~/.vatio/config.json`, or `$VATIO_HOME/config.json` — per developer, never
|
|
53
|
+
inside a repository. `VATIO_BASE_URL` and `VATIO_TOKEN` override it for one
|
|
54
|
+
command, which is what CI should use.
|
|
55
|
+
|
|
56
|
+
## Where the rules live
|
|
57
|
+
|
|
58
|
+
Whether a workspace is valid is answered by Vatio, not by this package: `push`
|
|
59
|
+
and `tools check` send the directory and read the result back. So the rules that
|
|
60
|
+
judge your workspace are always the deployed ones, and this CLI never has to be
|
|
61
|
+
upgraded to understand a new one.
|
package/bin/vatio.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The Vatio CLI, as an npm package: `npx vatio push`.
|
|
3
|
+
//
|
|
4
|
+
// Same platform, same API, same ~/.vatio/config.json as the Ruby CLI that
|
|
5
|
+
// `curl … /install.sh | bash` puts on a machine. Either can drive a workspace,
|
|
6
|
+
// and a developer who logged in with one is logged in for the other -- the
|
|
7
|
+
// credential belongs to their home directory, not to a client.
|
|
8
|
+
//
|
|
9
|
+
// This exists because Node is the runtime this audience already has and Ruby
|
|
10
|
+
// is the one some of them do not. Nothing about the contract moved: the
|
|
11
|
+
// workspace is parsed by the platform, so a client is HTTP and a terminal.
|
|
12
|
+
|
|
13
|
+
import { Config, ConfigError } from "../lib/config.mjs";
|
|
14
|
+
import { HttpError, UnauthorizedError, ForbiddenError, NotFoundError } from "../lib/http.mjs";
|
|
15
|
+
import { UserError, fail } from "../lib/support.mjs";
|
|
16
|
+
import { helpText } from "../lib/help.mjs";
|
|
17
|
+
import { login, logout } from "../lib/commands/auth.mjs";
|
|
18
|
+
import { configCommand, doctor, init, version } from "../lib/commands/misc.mjs";
|
|
19
|
+
import { diff, publish, push, rollback, status, toolsCheck } from "../lib/commands/deploy.mjs";
|
|
20
|
+
import { secrets, tokens, widget } from "../lib/commands/workspace-admin.mjs";
|
|
21
|
+
import { chat } from "../lib/commands/chat.mjs";
|
|
22
|
+
import { kb } from "../lib/commands/knowledge.mjs";
|
|
23
|
+
import { instagram, whatsapp } from "../lib/commands/channels.mjs";
|
|
24
|
+
import { docs, issue } from "../lib/commands/support-commands.mjs";
|
|
25
|
+
import { mcp } from "../lib/commands/mcp.mjs";
|
|
26
|
+
|
|
27
|
+
// Two commands that existed and are gone, answered by name rather than as
|
|
28
|
+
// unknown: one was removed from the product, the other is what npm does.
|
|
29
|
+
const RETIRED = {
|
|
30
|
+
pull: "was removed: a deployed manifest only ever comes from a push, so it could never\n" +
|
|
31
|
+
"hand back anything your repository did not already have. Use `vatio diff --env live`,\n" +
|
|
32
|
+
"or read the manifest at GET /api/v1/:slug/deploy/manifest.",
|
|
33
|
+
update: "is npm's job now: `npx vatio@latest` always runs the current release, and\n" +
|
|
34
|
+
"`npm install -g vatio@latest` updates a global install."
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
async function main(argv) {
|
|
38
|
+
const args = argv.slice(2);
|
|
39
|
+
const command = args.shift();
|
|
40
|
+
const config = new Config({ startDir: process.cwd() });
|
|
41
|
+
|
|
42
|
+
switch (command) {
|
|
43
|
+
case "version":
|
|
44
|
+
case "--version":
|
|
45
|
+
case "-v":
|
|
46
|
+
return version();
|
|
47
|
+
case "doctor":
|
|
48
|
+
return doctor(config);
|
|
49
|
+
case "login":
|
|
50
|
+
return await login(config, args);
|
|
51
|
+
case "logout":
|
|
52
|
+
return logout(config);
|
|
53
|
+
case "config":
|
|
54
|
+
return configCommand(config, args);
|
|
55
|
+
case "init":
|
|
56
|
+
return await init(config, args);
|
|
57
|
+
case "push":
|
|
58
|
+
return await push(config, args);
|
|
59
|
+
case "publish":
|
|
60
|
+
return await publish(config, args);
|
|
61
|
+
case "rollback":
|
|
62
|
+
return await rollback(config);
|
|
63
|
+
case "status":
|
|
64
|
+
return await status(config);
|
|
65
|
+
case "diff":
|
|
66
|
+
return await diff(config, args);
|
|
67
|
+
case "tools":
|
|
68
|
+
return await toolsCheck(config, args);
|
|
69
|
+
case "tokens":
|
|
70
|
+
return await tokens(config, args);
|
|
71
|
+
case "widget":
|
|
72
|
+
return await widget(config, args);
|
|
73
|
+
case "chat":
|
|
74
|
+
return await chat(config, args);
|
|
75
|
+
case "kb":
|
|
76
|
+
return await kb(config, args);
|
|
77
|
+
case "secrets":
|
|
78
|
+
return await secrets(config, args);
|
|
79
|
+
case "whatsapp":
|
|
80
|
+
return await whatsapp(config, args);
|
|
81
|
+
case "instagram":
|
|
82
|
+
return await instagram(config, args);
|
|
83
|
+
case "docs":
|
|
84
|
+
return await docs(config, args);
|
|
85
|
+
case "issue":
|
|
86
|
+
return await issue(config, args);
|
|
87
|
+
case "mcp":
|
|
88
|
+
return await mcp(config);
|
|
89
|
+
case "help":
|
|
90
|
+
case "-h":
|
|
91
|
+
case "--help":
|
|
92
|
+
case undefined:
|
|
93
|
+
return console.log(helpText());
|
|
94
|
+
default:
|
|
95
|
+
if (RETIRED[command]) fail(`\`vatio ${command}\` ${RETIRED[command]}`);
|
|
96
|
+
fail(`Unknown command: ${command}\n\n${helpText()}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
main(process.argv).catch((error) => {
|
|
102
|
+
if (error instanceof UserError || error instanceof ConfigError) {
|
|
103
|
+
console.error(error.message);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
if (error instanceof UnauthorizedError) {
|
|
107
|
+
console.error("Authentication failed (401). Run `vatio login`.");
|
|
108
|
+
if (error.requestId) console.error(`request_id=${error.requestId}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
if (error instanceof ForbiddenError) {
|
|
112
|
+
console.error("Access denied (403). Your token cannot access this workspace.");
|
|
113
|
+
if (error.requestId) console.error(`request_id=${error.requestId}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
if (error instanceof NotFoundError) {
|
|
117
|
+
console.error(
|
|
118
|
+
"Remote resource not found (404). Check the workspace slug; `vatio push` creates a missing remote automatically."
|
|
119
|
+
);
|
|
120
|
+
if (error.requestId) console.error(`request_id=${error.requestId}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
if (error instanceof HttpError) {
|
|
124
|
+
console.error(error.message);
|
|
125
|
+
if (error.requestId) console.error(`request_id=${error.requestId}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
// Anything left is a bug in this CLI rather than something the developer
|
|
129
|
+
// did, so it keeps its stack: that is what makes it reportable.
|
|
130
|
+
console.error(error?.stack ?? String(error));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Everything the CLI can ask of one workspace: /api/v1/:slug/*.
|
|
2
|
+
//
|
|
3
|
+
// One class rather than a client per surface. They share a host, a token and an
|
|
4
|
+
// error contract, and splitting them only ever meant passing the same three
|
|
5
|
+
// things around four times.
|
|
6
|
+
|
|
7
|
+
import { HttpClient } from "./http.mjs";
|
|
8
|
+
|
|
9
|
+
const encode = (value) => encodeURIComponent(String(value));
|
|
10
|
+
|
|
11
|
+
export class ApiClient extends HttpClient {
|
|
12
|
+
// --- deploy ---------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
deployStatus() {
|
|
15
|
+
return this.get("/deploy/status");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
deployedManifest({ environment = "live" } = {}) {
|
|
19
|
+
return this.get("/deploy/manifest", { environment });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Validation without deploying: send the directory, get back the manifest it
|
|
23
|
+
// builds, its errors and warnings, and the diff against `environment`. A
|
|
24
|
+
// workspace that fails to validate is a normal 200 with ok:false -- only a
|
|
25
|
+
// bundle the platform cannot read at all raises.
|
|
26
|
+
check({ files, environment = null }) {
|
|
27
|
+
const body = { files };
|
|
28
|
+
if (environment) body.environment = environment;
|
|
29
|
+
return this.post("/deploy/check", body);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pushPreview({ manifest, gitSha = null, createdBy = null, environment = null }) {
|
|
33
|
+
const body = { manifest };
|
|
34
|
+
if (gitSha) body.git_sha = gitSha;
|
|
35
|
+
if (createdBy) body.created_by = createdBy;
|
|
36
|
+
if (environment) body.environment = environment;
|
|
37
|
+
return this.put("/deploy/preview", body);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
publish({ createdBy = null, environment = null } = {}) {
|
|
41
|
+
const body = {};
|
|
42
|
+
if (createdBy) body.created_by = createdBy;
|
|
43
|
+
if (environment) body.environment = environment;
|
|
44
|
+
return this.post("/deploy/publish", body);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
rollback({ createdBy = null } = {}) {
|
|
48
|
+
const body = {};
|
|
49
|
+
if (createdBy) body.created_by = createdBy;
|
|
50
|
+
return this.post("/deploy/rollback", body);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
revisions() {
|
|
54
|
+
return this.get("/deploy/revisions");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
revision(id) {
|
|
58
|
+
return this.get(`/deploy/revisions/${encode(id)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- secrets --------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
listSecrets() {
|
|
64
|
+
return this.get("/deploy/secrets");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
upsertSecret({ key, value }) {
|
|
68
|
+
return this.put(`/deploy/secrets/${encode(key)}`, { value });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
deleteSecret({ key }) {
|
|
72
|
+
return this.delete(`/deploy/secrets/${encode(key)}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- knowledge bases ------------------------------------------------
|
|
76
|
+
|
|
77
|
+
knowledgeBases() {
|
|
78
|
+
return this.get("/knowledge_bases");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
knowledgeBase(name) {
|
|
82
|
+
return this.get(`/knowledge_bases/${encode(name)}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
createKnowledgeBase(name) {
|
|
86
|
+
return this.post("/knowledge_bases", { name });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
deleteKnowledgeBase(name) {
|
|
90
|
+
return this.delete(`/knowledge_bases/${encode(name)}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
addKnowledgeCrawl({ base, name, siteUrl, includes = [], excludes = [] }) {
|
|
94
|
+
return this.post(`/knowledge_bases/${encode(base)}/sources`, {
|
|
95
|
+
name,
|
|
96
|
+
site_url: siteUrl,
|
|
97
|
+
include: includes,
|
|
98
|
+
exclude: excludes
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The file's bytes travel in the body: the platform chunks and embeds, and
|
|
103
|
+
// it is the only copy there will be.
|
|
104
|
+
uploadKnowledgeDocument({ base, filename, content }) {
|
|
105
|
+
return this.post(`/knowledge_bases/${encode(base)}/sources`, { filename, content });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
deleteKnowledgeSource({ base, name }) {
|
|
109
|
+
return this.delete(`/knowledge_bases/${encode(base)}/sources/${encode(name)}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
reindexKnowledgeSource({ base, name }) {
|
|
113
|
+
return this.post(`/knowledge_bases/${encode(base)}/sources/${encode(name)}/reindex`, {});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- publishable tokens and the widget ------------------------------
|
|
117
|
+
|
|
118
|
+
listPublishableTokens() {
|
|
119
|
+
return this.get("/publishable_tokens");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
createPublishableToken({ environment, label = null }) {
|
|
123
|
+
const body = { environment };
|
|
124
|
+
if (String(label ?? "").trim() !== "") body.label = String(label).trim();
|
|
125
|
+
return this.post("/publishable_tokens", body);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
revokePublishableToken({ prefix }) {
|
|
129
|
+
return this.delete(`/publishable_tokens/${encode(prefix)}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
widget() {
|
|
133
|
+
return this.get("/widget");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- the shared WhatsApp preview ------------------------------------
|
|
137
|
+
|
|
138
|
+
listTestPhoneNumbers() {
|
|
139
|
+
return this.get("/test_phone_numbers");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
addTestPhoneNumber({ phoneNumber }) {
|
|
143
|
+
return this.post("/test_phone_numbers", { phone_number: phoneNumber });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
verifyTestPhoneNumber({ phoneNumber, code }) {
|
|
147
|
+
return this.post(`/test_phone_numbers/${waId(phoneNumber)}/verify`, { code });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
resendTestPhoneNumberCode({ phoneNumber }) {
|
|
151
|
+
return this.post(`/test_phone_numbers/${waId(phoneNumber)}/resend`, {});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
removeTestPhoneNumber({ phoneNumber }) {
|
|
155
|
+
return this.delete(`/test_phone_numbers/${waId(phoneNumber)}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- the workspace's own WhatsApp number ----------------------------
|
|
159
|
+
|
|
160
|
+
whatsappAccount() {
|
|
161
|
+
return this.get("/whatsapp_account");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Mints the URL for Meta's Embedded Signup and returns it; it connects
|
|
165
|
+
// nothing on its own. Same shape as instagramConnectLink.
|
|
166
|
+
whatsappConnectLink() {
|
|
167
|
+
return this.post("/whatsapp_account", {});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
checkWhatsappAccount() {
|
|
171
|
+
return this.post("/whatsapp_account/check", {});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
activateWhatsappAccount() {
|
|
175
|
+
return this.post("/whatsapp_account/activate", {});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
deactivateWhatsappAccount() {
|
|
179
|
+
return this.post("/whatsapp_account/deactivate", {});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
disconnectWhatsappAccount() {
|
|
183
|
+
return this.delete("/whatsapp_account");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- Instagram ------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
instagramAccount() {
|
|
189
|
+
return this.get("/instagram_account");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
instagramConnectLink() {
|
|
193
|
+
return this.post("/instagram_account", {});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
checkInstagramAccount() {
|
|
197
|
+
return this.post("/instagram_account/check", {});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
disconnectInstagramAccount() {
|
|
201
|
+
return this.delete("/instagram_account");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
listTestInstagramAccounts() {
|
|
205
|
+
return this.get("/test_instagram_accounts");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Declares which Instagram account you mean. The code comes later, by DM,
|
|
209
|
+
// once that account writes to the shared preview -- Instagram will not let
|
|
210
|
+
// Vatio message an account that has not messaged it first.
|
|
211
|
+
addTestInstagramAccount({ username }) {
|
|
212
|
+
return this.post("/test_instagram_accounts", { username });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Addressed by the code alone: the workspace is already in the path and the
|
|
216
|
+
// code is the only value the developer has in hand.
|
|
217
|
+
verifyTestInstagramAccount({ code }) {
|
|
218
|
+
return this.post("/test_instagram_accounts/verify", { code });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
showTestInstagramAccount(id) {
|
|
222
|
+
return this.get(`/test_instagram_accounts/${encode(id)}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
resendTestInstagramAccountCode(id) {
|
|
226
|
+
return this.post(`/test_instagram_accounts/${encode(id)}/resend_otp`, {});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
removeTestInstagramAccount(id) {
|
|
230
|
+
return this.delete(`/test_instagram_accounts/${encode(id)}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- chats ----------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
createChat({ environment }) {
|
|
236
|
+
return this.post("/chats", { environment });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
resetChat({ chatId, environment }) {
|
|
240
|
+
return this.post(`/chats/${encode(chatId)}/reset`, { environment });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
showChat({ chatId, view = "visitor" }) {
|
|
244
|
+
return this.get(`/chats/${encode(chatId)}`, { view });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
chatMessages({ chatId, view = "visitor", after = null, limit = null }) {
|
|
248
|
+
const params = { view };
|
|
249
|
+
if (after) params.after = after;
|
|
250
|
+
if (limit) params.limit = limit;
|
|
251
|
+
return this.get(`/chats/${encode(chatId)}/messages`, params);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
sendChatMessage({ chatId, content }) {
|
|
255
|
+
return this.post(`/chats/${encode(chatId)}/messages`, { content });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
destroyChat({ chatId }) {
|
|
259
|
+
return this.delete(`/chats/${encode(chatId)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// /cli/* -- not workspace-scoped, so it takes the bare host.
|
|
264
|
+
export class CliClient extends HttpClient {
|
|
265
|
+
listWorkspaces() {
|
|
266
|
+
return this.get("/cli/workspaces");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
createWorkspace({ slug, name = null }) {
|
|
270
|
+
const body = { slug };
|
|
271
|
+
if (String(name ?? "").trim() !== "") body.name = String(name).trim();
|
|
272
|
+
return this.post("/cli/workspaces", body);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async workspaceExists(slug) {
|
|
276
|
+
const payload = await this.listWorkspaces();
|
|
277
|
+
const workspaces = Array.isArray(payload.workspaces) ? payload.workspaces : [];
|
|
278
|
+
return workspaces.some((workspace) => String(workspace.slug) === String(slug));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// The path carries the digits: "+" and spaces do not survive a URL, and the
|
|
283
|
+
// digits are what the API stores as `wa_id`.
|
|
284
|
+
function waId(phoneNumber) {
|
|
285
|
+
return String(phoneNumber ?? "").replace(/\D/g, "");
|
|
286
|
+
}
|
package/lib/bundle.mjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The workspace directory as the bytes that go on the wire, for
|
|
2
|
+
// POST /api/v1/:slug/deploy/check. The port of cli/lib/workspace_bundle.rb.
|
|
3
|
+
//
|
|
4
|
+
// There is deliberately no list here of "the files a manifest reads". That list
|
|
5
|
+
// *is* the contract -- `widget.logo` and `identity.public_key` name files from
|
|
6
|
+
// inside vatio.yml -- and a client that knew it would be a client that needs a
|
|
7
|
+
// release every time the answer changes. So this walks what is on disk, skips
|
|
8
|
+
// what is plainly not workspace source, and lets the platform decide what
|
|
9
|
+
// matters.
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
12
|
+
import { join, relative, sep } from "node:path";
|
|
13
|
+
|
|
14
|
+
// Mirrors what /deploy/check enforces. Checked here as well so an oversized
|
|
15
|
+
// workspace fails in the terminal rather than after the upload.
|
|
16
|
+
export const MAX_FILES = 500;
|
|
17
|
+
export const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
export const MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
19
|
+
// Hidden entries are skipped wholesale, which is what keeps `.env` and `.git/`
|
|
20
|
+
// out of an upload without having to name either.
|
|
21
|
+
export const SKIP_DIRECTORIES = new Set(["node_modules", "tmp", "log"]);
|
|
22
|
+
|
|
23
|
+
export class BundleError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "BundleError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildBundle(root) {
|
|
31
|
+
if (!isDirectory(root)) throw new BundleError(`not a directory: ${root}`);
|
|
32
|
+
|
|
33
|
+
const files = [];
|
|
34
|
+
let total = 0;
|
|
35
|
+
|
|
36
|
+
for (const path of walk(root)) {
|
|
37
|
+
// Posix separators on the wire: the platform materializes these into a
|
|
38
|
+
// directory and a Windows client must not send backslashes for it to
|
|
39
|
+
// refuse.
|
|
40
|
+
const relativePath = relative(root, path).split(sep).join("/");
|
|
41
|
+
const bytes = readFileSync(path);
|
|
42
|
+
|
|
43
|
+
if (bytes.byteLength > MAX_FILE_BYTES) {
|
|
44
|
+
throw new BundleError(`${relativePath} is ${bytes.byteLength} bytes; the limit is ${MAX_FILE_BYTES}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
total += bytes.byteLength;
|
|
48
|
+
if (total > MAX_TOTAL_BYTES) throw new BundleError(`the workspace is over ${MAX_TOTAL_BYTES} bytes`);
|
|
49
|
+
if (files.length >= MAX_FILES) throw new BundleError(`the workspace has more than ${MAX_FILES} files`);
|
|
50
|
+
|
|
51
|
+
files.push(entryFor(relativePath, bytes));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (files.length === 0) throw new BundleError("the workspace is empty");
|
|
55
|
+
return files;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function* walk(dir) {
|
|
59
|
+
const children = readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
60
|
+
for (const child of children) {
|
|
61
|
+
if (child.name.startsWith(".")) continue;
|
|
62
|
+
|
|
63
|
+
const path = join(dir, child.name);
|
|
64
|
+
if (child.isDirectory()) {
|
|
65
|
+
if (SKIP_DIRECTORIES.has(child.name)) continue;
|
|
66
|
+
yield* walk(path);
|
|
67
|
+
} else if (child.isFile()) {
|
|
68
|
+
yield path;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Text travels as text so a diff of the request is readable and a logo does not
|
|
74
|
+
// corrupt on the way. Anything that is not valid UTF-8 -- a png, a compiled
|
|
75
|
+
// asset someone left in the directory -- goes as base64.
|
|
76
|
+
function entryFor(path, bytes) {
|
|
77
|
+
const text = bytes.toString("utf8");
|
|
78
|
+
if (!text.includes("�") && !text.includes("\0") && Buffer.compare(Buffer.from(text, "utf8"), bytes) === 0) {
|
|
79
|
+
return { path, content: text, encoding: "utf-8" };
|
|
80
|
+
}
|
|
81
|
+
return { path, content: bytes.toString("base64"), encoding: "base64" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isDirectory(path) {
|
|
85
|
+
try {
|
|
86
|
+
return statSync(path).isDirectory();
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// login / logout, and the device-code dance they share with `init`.
|
|
2
|
+
|
|
3
|
+
import { DeviceAuthClient, ExpiredError, PendingError } from "../device-auth.mjs";
|
|
4
|
+
import { DEFAULT_BASE_URL } from "../config.mjs";
|
|
5
|
+
import { fail, openBrowser, sleep, takeValue } from "../support.mjs";
|
|
6
|
+
|
|
7
|
+
// A token is minted by a human approving a code in a browser, not by anything
|
|
8
|
+
// the CLI can do on its own. That is the whole point: a secret this CLI holds
|
|
9
|
+
// was authorized by someone who was there.
|
|
10
|
+
export async function deviceLogin({ baseUrl, message }) {
|
|
11
|
+
const resolved = String(baseUrl || process.env.VATIO_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
12
|
+
const client = new DeviceAuthClient({ baseUrl: resolved });
|
|
13
|
+
|
|
14
|
+
console.log(message);
|
|
15
|
+
const auth = await client.start();
|
|
16
|
+
const deviceCode = auth.device_code;
|
|
17
|
+
const userCode = auth.user_code;
|
|
18
|
+
const verificationUri = auth.verification_uri_complete ?? auth.verification_uri;
|
|
19
|
+
const interval = Number(auth.interval ?? 5);
|
|
20
|
+
const expiresIn = Number(auth.expires_in ?? 900);
|
|
21
|
+
if (!deviceCode || !verificationUri) fail("The platform did not return a device code");
|
|
22
|
+
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log(`Open: ${verificationUri}`);
|
|
25
|
+
console.log(`Code: ${userCode}`);
|
|
26
|
+
console.log("");
|
|
27
|
+
openBrowser(verificationUri);
|
|
28
|
+
|
|
29
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
30
|
+
for (;;) {
|
|
31
|
+
if (Date.now() > deadline) fail("Timed out waiting for authorization");
|
|
32
|
+
try {
|
|
33
|
+
const result = await client.poll({ deviceCode });
|
|
34
|
+
return {
|
|
35
|
+
token: result.token,
|
|
36
|
+
baseUrl: String(result.base_url ?? "").trim() || resolved
|
|
37
|
+
};
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof PendingError) {
|
|
40
|
+
await sleep(interval * 1000);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (error instanceof ExpiredError) fail("The code expired. Run `vatio login` again.");
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function login(config, args) {
|
|
50
|
+
const baseUrl = takeValue(args, "--base-url");
|
|
51
|
+
const { token, baseUrl: resolved } = await deviceLogin({
|
|
52
|
+
baseUrl,
|
|
53
|
+
message: "Starting device authorization for Vatio CLI…"
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
config.writeAuth({ baseUrl: resolved, token });
|
|
57
|
+
console.log(`Logged in to ${resolved}`);
|
|
58
|
+
console.log(`Token saved to ${config.configPath}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function logout(config) {
|
|
62
|
+
config.clearToken();
|
|
63
|
+
console.log(`Removed the token from ${config.configPath}`);
|
|
64
|
+
}
|