javer-cli 1.0.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/LICENSE +21 -0
- package/README.md +154 -0
- package/index.js +444 -0
- package/lib/api.js +79 -0
- package/lib/bird.js +129 -0
- package/lib/config.js +43 -0
- package/lib/ui.js +366 -0
- package/lib/zip.js +174 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javer Studios
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# javer-cli
|
|
2
|
+
|
|
3
|
+
Javer Webhost from the command line — apps, virtual machines, databases and
|
|
4
|
+
domains, without opening the panel.
|
|
5
|
+
|
|
6
|
+
No dependencies. Node 18 or newer.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g javer-cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Getting a key
|
|
13
|
+
|
|
14
|
+
Create one at [panel.javer.pro/api-keys](https://panel.javer.pro/api-keys), then:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
javer login
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
It saves to `~/.javer/config.json`, readable only by you.
|
|
21
|
+
|
|
22
|
+
A key does not expire and is not scoped — it can do anything your account can.
|
|
23
|
+
Revoke it from the panel when you are finished with it; `javer logout` only
|
|
24
|
+
forgets the local copy.
|
|
25
|
+
|
|
26
|
+
## The live dashboard
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
javer ui
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Apps and virtual machines on one screen, with the resource budget they share
|
|
33
|
+
across the top. Arrow keys select; `s` starts or stops, `r` restarts, `l` drops
|
|
34
|
+
to logs, `q` quits. It refreshes every four seconds.
|
|
35
|
+
|
|
36
|
+
Stopping or restarting asks first.
|
|
37
|
+
|
|
38
|
+
## Apps
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
javer ls # everything you are running
|
|
42
|
+
javer status <app> # size, host, address
|
|
43
|
+
javer logs <app> # recent output
|
|
44
|
+
javer logs <app> -f # keep watching
|
|
45
|
+
javer env <app> # values hidden
|
|
46
|
+
javer env <app> --show # values in full
|
|
47
|
+
javer domains <app> # custom domains and whether they verified
|
|
48
|
+
javer start|stop|restart <app>
|
|
49
|
+
javer delete <app> # asks you to type the name
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`logs -f` polls every two seconds and prints what is new. The server returns the
|
|
53
|
+
last 300 lines as a snapshot rather than a stream, so this is a poll, not a tail.
|
|
54
|
+
|
|
55
|
+
## Deploying
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
javer deploy # this folder
|
|
59
|
+
javer deploy --repo owner/name # a connected GitHub repo
|
|
60
|
+
javer deploy --repo owner/name --branch main --ram 512 --cpu 0.5
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
With no `--repo` it packs the directory you are standing in and uploads it. The
|
|
64
|
+
folder name becomes the app name unless you pass `--name`.
|
|
65
|
+
|
|
66
|
+
Left out of the upload automatically: `node_modules` (the server installs
|
|
67
|
+
dependencies itself), `.git`, build output, logs, and `.env` — environment
|
|
68
|
+
variables belong in the panel, not baked into a deployment.
|
|
69
|
+
|
|
70
|
+
Uploads are capped at **2 MB** after compression. Past that, deploy from GitHub.
|
|
71
|
+
|
|
72
|
+
Two things the platform requires, worth knowing before your first deploy:
|
|
73
|
+
|
|
74
|
+
- **Read `process.env.PORT`.** Javer assigns the port. An app that hardcodes one
|
|
75
|
+
starts, looks healthy, and returns 502, because nothing is listening where the
|
|
76
|
+
traffic is being sent.
|
|
77
|
+
- **The entry file must be `server.js`** at the root, or `index.html` for a
|
|
78
|
+
static site. A `start` script in `package.json` is not read.
|
|
79
|
+
|
|
80
|
+
## Virtual machines
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
javer vms # list them
|
|
84
|
+
javer vm <name> # details, including live memory and CPU
|
|
85
|
+
javer vm start|stop|restart <name>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
SSH from your own machine is not available — VMs sit on a private network with
|
|
89
|
+
no route in. Use the browser terminal in the panel.
|
|
90
|
+
|
|
91
|
+
## Databases
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
javer db # list them
|
|
95
|
+
javer db create <name> # PostgreSQL
|
|
96
|
+
javer db create <name> --engine mysql
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Credentials print once, on creation, and are not stored anywhere they can be
|
|
100
|
+
read back. A database is reachable only from inside Javer, so you cannot connect
|
|
101
|
+
from your laptop with `psql` or TablePlus.
|
|
102
|
+
|
|
103
|
+
## Your plan
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
javer whoami
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Shows the account's RAM, CPU and slot budget and what is left. The item count
|
|
110
|
+
includes virtual machines as well as apps.
|
|
111
|
+
|
|
112
|
+
## In a pipeline
|
|
113
|
+
|
|
114
|
+
Set `JAVER_API_KEY` instead of logging in, so nothing is written to disk:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
JAVER_API_KEY=<key> javer deploy --repo myorg/myapp --branch main
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`javer deploy` follows the build and exits non-zero if it fails, so it works as
|
|
121
|
+
a build step. `javer delete` refuses to run without a terminal to confirm in
|
|
122
|
+
unless you pass `--yes`.
|
|
123
|
+
|
|
124
|
+
## Pointing somewhere else
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
JAVER_API_URL=http://localhost:3000 javer ls
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Both variables take precedence over the saved file.
|
|
131
|
+
|
|
132
|
+
## Full command list
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
javer ui live dashboard: apps + VMs
|
|
136
|
+
javer login [--key <key>] [--url <url>] save an API key
|
|
137
|
+
javer logout forget the saved key locally
|
|
138
|
+
javer whoami plan limits and what is left
|
|
139
|
+
javer ls list your apps
|
|
140
|
+
javer status <app> details for one app
|
|
141
|
+
javer logs <app> [-f] recent output; -f keeps watching
|
|
142
|
+
javer env <app> [--show] environment variables
|
|
143
|
+
javer domains <app> custom domains
|
|
144
|
+
javer start|stop|restart <app> lifecycle
|
|
145
|
+
javer delete <app> [--yes] remove an app, its data and its URL
|
|
146
|
+
javer vms list virtual machines
|
|
147
|
+
javer vm <name> details for one VM
|
|
148
|
+
javer vm start|stop|restart <name> VM lifecycle
|
|
149
|
+
javer db list databases
|
|
150
|
+
javer db create <name> [--engine mysql] new Postgres or MySQL
|
|
151
|
+
javer deploy deploy the folder you are in
|
|
152
|
+
javer deploy --repo owner/name deploy from a connected GitHub repo
|
|
153
|
+
javer version | help
|
|
154
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// javer — command-line interface for Javer Webhost.
|
|
3
|
+
//
|
|
4
|
+
// Authenticates with an API key (created at panel.javer.pro/api-keys) rather
|
|
5
|
+
// than email+password: the server's requireAuth accepts a `jvr_live_` bearer
|
|
6
|
+
// token on every route the dashboard uses, so anything you can click you can
|
|
7
|
+
// script.
|
|
8
|
+
const config = require('./lib/config');
|
|
9
|
+
const ui = require('./lib/ui');
|
|
10
|
+
const { zipDir } = require('./lib/zip');
|
|
11
|
+
const bird = require('./lib/bird');
|
|
12
|
+
const api = require('./lib/api');
|
|
13
|
+
const { version } = require('./package.json');
|
|
14
|
+
|
|
15
|
+
// ── arg parsing ────────────────────────────────────────────────────────
|
|
16
|
+
// Hand-rolled rather than a dependency: the grammar is `javer <cmd> [pos]
|
|
17
|
+
// [--flag value]`, which is a dozen lines. A parser library would be most of
|
|
18
|
+
// this package's install weight.
|
|
19
|
+
const parseArgs = (argv) => {
|
|
20
|
+
const positional = [];
|
|
21
|
+
const flags = {};
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
if (argv[i].startsWith('--')) {
|
|
24
|
+
const name = argv[i].slice(2);
|
|
25
|
+
const next = argv[i + 1];
|
|
26
|
+
if (next === undefined || next.startsWith('--')) { flags[name] = true; }
|
|
27
|
+
else { flags[name] = next; i++; }
|
|
28
|
+
} else {
|
|
29
|
+
positional.push(argv[i]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { positional, flags };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const die = (msg, code = 1) => { console.error(`error: ${msg}`); process.exit(code); };
|
|
36
|
+
|
|
37
|
+
// Both deploy paths — a GitHub repo and this folder — start a build and then
|
|
38
|
+
// watch it the same way, so the watching lives here once.
|
|
39
|
+
const followDeploy = async (started) => {
|
|
40
|
+
const deployId = started.deployment?.id;
|
|
41
|
+
if (!deployId) {
|
|
42
|
+
// The server answered with a shape we do not know. Show it rather than
|
|
43
|
+
// pretending it worked or crashing on undefined.
|
|
44
|
+
console.log(JSON.stringify(started, null, 2));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let last = '';
|
|
48
|
+
for (;;) {
|
|
49
|
+
const { deployment } = await api.get(`/apps/deployments/${deployId}`);
|
|
50
|
+
if (deployment.step && deployment.step !== last) { console.log(deployment.step); last = deployment.step; }
|
|
51
|
+
if (deployment.status === 'ready') return console.log(`Live: ${deployment.url}`);
|
|
52
|
+
if (deployment.status === 'failed') die(deployment.error || 'Deploy failed.');
|
|
53
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Resolve a human-typed app name to its id. Names are what people know; ids
|
|
58
|
+
// are UUIDs nobody types. Ambiguity is reported rather than guessed at.
|
|
59
|
+
const resolveApp = async (name) => {
|
|
60
|
+
if (!name) die('Missing app name. Try `javer ls` to see your apps.');
|
|
61
|
+
const { apps } = await api.get('/apps/');
|
|
62
|
+
const exact = apps.filter((c) => c.name === name);
|
|
63
|
+
if (exact.length === 1) return exact[0];
|
|
64
|
+
if (exact.length > 1) die(`"${name}" matches ${exact.length} apps — this shouldn't happen; use the dashboard.`);
|
|
65
|
+
const near = apps.filter((c) => c.name.includes(name)).map((c) => c.name);
|
|
66
|
+
die(near.length
|
|
67
|
+
? `No app called "${name}". Did you mean: ${near.join(', ')}?`
|
|
68
|
+
: `No app called "${name}". Run \`javer ls\` to see your apps.`);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// VMs need the same name-to-id courtesy apps get. Kept separate rather than
|
|
72
|
+
// generalised: the two endpoints return different shapes, and a shared helper
|
|
73
|
+
// that guesses which one it is would be worse than two short ones.
|
|
74
|
+
const resolveVm = async (name) => {
|
|
75
|
+
if (!name) die('Missing VM name. Try `javer vms` to see them.');
|
|
76
|
+
const res = await api.get('/vms/');
|
|
77
|
+
const vms = res.vms || res.machines || [];
|
|
78
|
+
const exact = vms.filter((v) => v.name === name);
|
|
79
|
+
if (exact.length === 1) return exact[0];
|
|
80
|
+
const near = vms.filter((v) => v.name.includes(name)).map((v) => v.name);
|
|
81
|
+
die(near.length
|
|
82
|
+
? `No VM called "${name}". Did you mean: ${near.join(', ')}?`
|
|
83
|
+
: `No VM called "${name}". Run \`javer vms\` to see them.`);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const pad = (s, n) => String(s ?? '').padEnd(n);
|
|
87
|
+
|
|
88
|
+
// ── commands ───────────────────────────────────────────────────────────
|
|
89
|
+
const commands = {
|
|
90
|
+
async login(_pos, flags) {
|
|
91
|
+
let key = flags.key || process.env.JAVER_API_KEY;
|
|
92
|
+
if (!key) {
|
|
93
|
+
// Read from stdin so the key never lands in shell history the way a
|
|
94
|
+
// --key argument would.
|
|
95
|
+
process.stdout.write('Paste your API key (panel.javer.pro/api-keys): ');
|
|
96
|
+
key = await new Promise((resolve) => {
|
|
97
|
+
let buf = '';
|
|
98
|
+
process.stdin.setEncoding('utf-8');
|
|
99
|
+
// Resolve on the first NEWLINE, not on 'end'. In a terminal 'end' only
|
|
100
|
+
// fires on Ctrl-D, so pressing Enter after pasting did nothing at all —
|
|
101
|
+
// the prompt just sat there forever and the only way out was Ctrl-C.
|
|
102
|
+
// 'end' is still handled underneath so a piped key still works:
|
|
103
|
+
// echo "$KEY" | javer login
|
|
104
|
+
const done = (value) => {
|
|
105
|
+
process.stdin.removeListener('data', onData);
|
|
106
|
+
process.stdin.pause();
|
|
107
|
+
resolve(value.trim());
|
|
108
|
+
};
|
|
109
|
+
const onData = (d) => {
|
|
110
|
+
buf += d;
|
|
111
|
+
const nl = buf.indexOf('\n');
|
|
112
|
+
if (nl !== -1) done(buf.slice(0, nl));
|
|
113
|
+
};
|
|
114
|
+
process.stdin.on('data', onData);
|
|
115
|
+
process.stdin.once('end', () => done(buf));
|
|
116
|
+
});
|
|
117
|
+
process.stdout.write('\n');
|
|
118
|
+
}
|
|
119
|
+
key = String(key).trim();
|
|
120
|
+
if (!key.startsWith('jvr_live_')) die('That does not look like a Javer API key (they start with jvr_live_).');
|
|
121
|
+
|
|
122
|
+
const url = flags.url || config.apiUrl();
|
|
123
|
+
// Validate before saving: writing a bad key and only discovering it on
|
|
124
|
+
// the next command is a worse first experience than failing here.
|
|
125
|
+
config.write({ apiUrl: url, key });
|
|
126
|
+
try {
|
|
127
|
+
const usage = await api.get('/account/usage');
|
|
128
|
+
console.log(`Logged in to ${url}`);
|
|
129
|
+
console.log(`Plan allows ${usage.budget.ramMB} MB RAM, ${usage.budget.cpu} CPU, ${usage.budget.maxItems} apps.`);
|
|
130
|
+
console.log(`Key saved to ${config.FILE} (readable only by you).`);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
config.clear();
|
|
133
|
+
die(`Key rejected: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async logout() {
|
|
138
|
+
const had = config.clear();
|
|
139
|
+
// Say what actually happened. The key stays valid server-side — only the
|
|
140
|
+
// local copy is gone — and pretending otherwise is the same lie the
|
|
141
|
+
// GitHub disconnect used to tell.
|
|
142
|
+
console.log(had ? 'Local key removed.' : 'No saved key to remove.');
|
|
143
|
+
console.log('The key itself is still valid. Revoke it at panel.javer.pro/api-keys.');
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async whoami() {
|
|
147
|
+
const u = await api.get('/account/usage');
|
|
148
|
+
console.log(`Server : ${config.apiUrl()}`);
|
|
149
|
+
console.log(`Using : ${u.used.usedRamMB} MB RAM, ${u.used.usedCpu} CPU, ${u.used.usedItems} apps`);
|
|
150
|
+
console.log(`Limit : ${u.budget.ramMB} MB RAM, ${u.budget.cpu} CPU, ${u.budget.maxItems} apps`);
|
|
151
|
+
console.log(`Left : ${u.remaining.ramMB} MB RAM, ${u.remaining.cpu} CPU, ${u.remaining.items} apps`);
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
async ls() {
|
|
155
|
+
const { apps } = await api.get('/apps/');
|
|
156
|
+
if (!apps.length) return console.log('No apps yet. Deploy one with `javer deploy --repo owner/name`.');
|
|
157
|
+
console.log(`${pad('NAME', 24)}${pad('STATUS', 10)}${pad('RAM', 8)}URL`);
|
|
158
|
+
for (const c of apps) {
|
|
159
|
+
console.log(`${pad(c.name, 24)}${pad(c.status, 10)}${pad(c.ram_mb + 'MB', 8)}${c.hostname ? 'https://' + c.hostname : '-'}`);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async status(pos) {
|
|
164
|
+
const c = await resolveApp(pos[0]);
|
|
165
|
+
const full = await api.get(`/apps/${c.id}`);
|
|
166
|
+
const d = full.app || full;
|
|
167
|
+
console.log(`Name : ${d.name}`);
|
|
168
|
+
console.log(`Status : ${d.status}`);
|
|
169
|
+
console.log(`Image : ${d.image}`);
|
|
170
|
+
console.log(`Size : ${d.ram_mb} MB RAM, ${d.cpu} CPU`);
|
|
171
|
+
console.log(`Host : ${d.docker_host}`);
|
|
172
|
+
if (c.hostname) console.log(`URL : https://${c.hostname}`);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
async logs(pos, flags) {
|
|
176
|
+
const c = await resolveApp(pos[0]);
|
|
177
|
+
const fetchLogs = async () => (await api.get(`/apps/${c.id}/logs`)).logs || '';
|
|
178
|
+
|
|
179
|
+
if (!flags.f && !flags.follow) {
|
|
180
|
+
process.stdout.write(await fetchLogs() || '(no output)\n');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Honest about what this is: the endpoint returns the last 300 lines as
|
|
185
|
+
// JSON, there is no stream to attach to, so this polls and prints what is
|
|
186
|
+
// new. Calling it "streaming" would be a lie the first time somebody
|
|
187
|
+
// noticed the two-second granularity.
|
|
188
|
+
console.log(`Polling ${c.name} every 2s — Ctrl-C to stop.\n`);
|
|
189
|
+
let lastLine = null;
|
|
190
|
+
for (;;) {
|
|
191
|
+
let lines;
|
|
192
|
+
try {
|
|
193
|
+
lines = (await fetchLogs()).split('\n');
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// A blip should not end the session — say so and keep watching.
|
|
196
|
+
console.error(` (${err.message} — retrying)`);
|
|
197
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (lastLine === null) {
|
|
201
|
+
process.stdout.write(lines.join('\n'));
|
|
202
|
+
} else {
|
|
203
|
+
// Find where we left off. Lines carry timestamps so they are
|
|
204
|
+
// effectively unique; if the line has scrolled out of the 300-line
|
|
205
|
+
// window it is gone, and printing the whole window beats printing
|
|
206
|
+
// nothing and looking frozen.
|
|
207
|
+
const at = lines.lastIndexOf(lastLine);
|
|
208
|
+
const fresh = at === -1 ? lines : lines.slice(at + 1);
|
|
209
|
+
if (fresh.length && fresh.some((l) => l.trim())) process.stdout.write(fresh.join('\n'));
|
|
210
|
+
}
|
|
211
|
+
lastLine = lines.filter((l) => l.trim()).pop() ?? lastLine;
|
|
212
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
async env(pos, flags) {
|
|
217
|
+
const c = await resolveApp(pos[0]);
|
|
218
|
+
const { env } = await api.get(`/apps/${c.id}/env`);
|
|
219
|
+
const keys = Object.keys(env);
|
|
220
|
+
if (!keys.length) return console.log('(no environment variables set)');
|
|
221
|
+
// Masked unless asked. Environment variables are where API keys and
|
|
222
|
+
// database passwords live, and this command gets run in shared terminals,
|
|
223
|
+
// screen shares and CI logs where the output outlives the moment. Printing
|
|
224
|
+
// them by default makes leaking one the path of least resistance.
|
|
225
|
+
for (const k of keys) {
|
|
226
|
+
const v = String(env[k]);
|
|
227
|
+
console.log(`${k}=${flags.show ? v : (v.length > 6 ? v.slice(0, 3) + '\u2026' + v.slice(-2) : '\u2026')}`);
|
|
228
|
+
}
|
|
229
|
+
if (!flags.show) console.log('\n(values hidden \u2014 pass --show to print them in full)');
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
async start(pos) { await lifecycle(pos[0], 'start'); },
|
|
233
|
+
async stop(pos) { await lifecycle(pos[0], 'stop'); },
|
|
234
|
+
async restart(pos) { await lifecycle(pos[0], 'restart'); },
|
|
235
|
+
|
|
236
|
+
async deploy(_pos, flags) {
|
|
237
|
+
const repo = flags.repo;
|
|
238
|
+
|
|
239
|
+
// No --repo means "deploy the folder I am standing in". Previously the
|
|
240
|
+
// only way to ship code was a connected GitHub repo, so there was no path
|
|
241
|
+
// at all from a working directory to a running app without the panel.
|
|
242
|
+
if (!repo) {
|
|
243
|
+
const cwd = process.cwd();
|
|
244
|
+
const name = flags.name || require('path').basename(cwd).toLowerCase()
|
|
245
|
+
.replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, '').slice(0, 30);
|
|
246
|
+
if (!name) die('Could not make an app name from this folder — pass --name.');
|
|
247
|
+
|
|
248
|
+
process.stdout.write(`Packing ${cwd}… `);
|
|
249
|
+
let archive;
|
|
250
|
+
try { archive = zipDir(cwd); } catch (err) { console.log(''); die(err.message); }
|
|
251
|
+
const kb = Math.round(archive.buffer.length / 1024);
|
|
252
|
+
console.log(`${archive.fileCount} files, ${kb} KB`);
|
|
253
|
+
|
|
254
|
+
// The server caps uploads at 2 MB. Fail here with something actionable
|
|
255
|
+
// rather than letting it come back as a bare HTTP 413.
|
|
256
|
+
if (archive.buffer.length > 2 * 1024 * 1024) {
|
|
257
|
+
die(`That is ${kb} KB and the limit is 2048 KB. Remove large files, or deploy from GitHub with --repo owner/name.`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const fields = { name, async: 'true' };
|
|
261
|
+
if (flags.ram) fields.ramMB = Number(flags.ram);
|
|
262
|
+
if (flags.cpu) fields.cpu = Number(flags.cpu);
|
|
263
|
+
const startedUpload = await api.upload('/apps/deployments', 'app.zip', archive.buffer, fields);
|
|
264
|
+
return await followDeploy(startedUpload);
|
|
265
|
+
}
|
|
266
|
+
if (typeof repo !== 'string') die('Usage: javer deploy --repo owner/name (or no --repo to deploy this folder)');
|
|
267
|
+
// async:true asks the server to answer with the deployment and build it
|
|
268
|
+
// detached, so this can print progress. Without it the request blocked
|
|
269
|
+
// until the build finished and the id this polls for never came back —
|
|
270
|
+
// which is why `javer deploy` had been printing raw JSON.
|
|
271
|
+
const body = { source: 'github', repoFullName: repo, async: true };
|
|
272
|
+
if (flags.name) body.name = flags.name;
|
|
273
|
+
if (flags.branch) body.branch = flags.branch;
|
|
274
|
+
if (flags.ram) body.ramMB = Number(flags.ram);
|
|
275
|
+
if (flags.cpu) body.cpu = Number(flags.cpu);
|
|
276
|
+
|
|
277
|
+
return await followDeploy(await api.post('/apps/deployments', body));
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
// A live view of apps and VMs together. Its own module — a full-screen
|
|
282
|
+
// interface has nothing in common with a command that prints and exits.
|
|
283
|
+
async ui() { await ui.run(); },
|
|
284
|
+
|
|
285
|
+
async vms() {
|
|
286
|
+
const res = await api.get('/vms/');
|
|
287
|
+
const vms = res.vms || res.machines || [];
|
|
288
|
+
if (!vms.length) return console.log('No VMs yet. Create one at panel.javer.pro/vms.');
|
|
289
|
+
console.log(`${pad('NAME', 24)}${pad('STATUS', 12)}${pad('RAM', 9)}${pad('CPU', 5)}${pad('DISK', 8)}ADDRESS`);
|
|
290
|
+
for (const v of vms) {
|
|
291
|
+
console.log(`${pad(v.name, 24)}${pad(v.state || v.status, 12)}${pad((v.ram_mb || '?') + 'MB', 9)}` +
|
|
292
|
+
`${pad(v.cpu ?? '?', 5)}${pad((v.disk_gb || '?') + 'GB', 8)}${v.ip || '-'}`);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
// `javer vm <name>` shows one; `javer vm start|stop|restart <name>` acts.
|
|
297
|
+
// Reads as English either way, and there is no separate verb to remember.
|
|
298
|
+
async vm(pos) {
|
|
299
|
+
const VERBS = ['start', 'stop', 'restart'];
|
|
300
|
+
if (VERBS.includes(pos[0])) {
|
|
301
|
+
const v = await resolveVm(pos[1]);
|
|
302
|
+
await api.post(`/vms/${v.id}/${pos[0]}`, {});
|
|
303
|
+
return console.log(`${pos[0]} sent to ${v.name}.`);
|
|
304
|
+
}
|
|
305
|
+
const v = await resolveVm(pos[0]);
|
|
306
|
+
const full = await api.get(`/vms/${v.id}`).catch(() => null);
|
|
307
|
+
const d = (full && (full.vm || full)) || v;
|
|
308
|
+
console.log(`Name : ${d.name}`);
|
|
309
|
+
console.log(`Status : ${d.state || d.status}`);
|
|
310
|
+
console.log(`Size : ${d.ram_mb} MB RAM, ${d.cpu} CPU, ${d.disk_gb} GB disk`);
|
|
311
|
+
console.log(`OS : ${d.os_image || d.image || '-'}`);
|
|
312
|
+
console.log(`Address: ${d.ip || '-'}`);
|
|
313
|
+
if ((d.state || d.status) === 'running') {
|
|
314
|
+
const s = await api.get(`/vms/${d.id}/stats`).catch(() => null);
|
|
315
|
+
if (s) console.log(`Live : ${Math.round((s.memUsage || 0) / 1048576)} MB of ${Math.round((s.memLimit || 0) / 1048576)} MB, CPU ${s.cpuPercent}%`);
|
|
316
|
+
}
|
|
317
|
+
console.log('\nSSH from your own machine is not available — open the terminal in the panel.');
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
async db(pos, flags) {
|
|
321
|
+
if (pos[0] === 'create') {
|
|
322
|
+
const engine = (flags.engine || 'postgres').toLowerCase();
|
|
323
|
+
const name = flags.name || pos[1];
|
|
324
|
+
if (!name) die('Usage: javer db create <name> [--engine postgres|mysql]');
|
|
325
|
+
const res = await api.post('/apps/database', { name, engine });
|
|
326
|
+
const c = res.credentials || res;
|
|
327
|
+
console.log('Database created. These credentials are shown ONCE:\n');
|
|
328
|
+
for (const k of ['host', 'port', 'user', 'password', 'database', 'url']) {
|
|
329
|
+
if (c[k]) console.log(` ${pad(k, 10)}${c[k]}`);
|
|
330
|
+
}
|
|
331
|
+
console.log('\nIt is reachable only from inside Javer — not from this machine.');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const { apps } = await api.get('/apps/');
|
|
335
|
+
const dbs = apps.filter((a) => a.kind === 'database');
|
|
336
|
+
if (!dbs.length) return console.log('No databases. Create one with `javer db create <name>`.');
|
|
337
|
+
console.log(`${pad('NAME', 24)}${pad('STATUS', 10)}ENGINE`);
|
|
338
|
+
for (const d of dbs) console.log(`${pad(d.name, 24)}${pad(d.status, 10)}${d.image || '-'}`);
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
async domains(pos) {
|
|
342
|
+
const c = await resolveApp(pos[0]);
|
|
343
|
+
const res = await api.get(`/apps/${c.id}/domains`);
|
|
344
|
+
const list = res.domains || [];
|
|
345
|
+
if (!list.length) return console.log(`No custom domains on ${c.name}. Add one in the panel.`);
|
|
346
|
+
for (const d of list) {
|
|
347
|
+
console.log(`${pad(d.hostname, 34)}${d.verified ? 'ownership verified' : 'NOT verified'}`);
|
|
348
|
+
}
|
|
349
|
+
console.log('\nNote: your domain\'s DNS must be on Cloudflare for routing to work.');
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
// Deleting takes the app, its volumes, its DNS record and its data, and
|
|
353
|
+
// there is no undo anywhere in the product. So it asks you to type the name
|
|
354
|
+
// — the same bar the panel sets — rather than accepting a y/n, which people
|
|
355
|
+
// answer reflexively. --yes exists for scripts that genuinely mean it.
|
|
356
|
+
async delete(pos, flags) {
|
|
357
|
+
const c = await resolveApp(pos[0]);
|
|
358
|
+
if (!flags.yes && !flags.y) {
|
|
359
|
+
if (!process.stdin.isTTY) {
|
|
360
|
+
die(`Refusing to delete ${c.name} without a terminal to confirm in. Pass --yes if you mean it.`);
|
|
361
|
+
}
|
|
362
|
+
console.log(`This permanently deletes ${c.name}, its data and its URL. There is no undo.`);
|
|
363
|
+
process.stdout.write(`Type the name to confirm: `);
|
|
364
|
+
const typed = await new Promise((resolve) => {
|
|
365
|
+
let buf = '';
|
|
366
|
+
process.stdin.setEncoding('utf-8');
|
|
367
|
+
const done = (v) => { process.stdin.removeListener('data', onData); process.stdin.pause(); resolve(v.trim()); };
|
|
368
|
+
const onData = (d) => { buf += d; const nl = buf.indexOf('\n'); if (nl !== -1) done(buf.slice(0, nl)); };
|
|
369
|
+
process.stdin.on('data', onData);
|
|
370
|
+
process.stdin.once('end', () => done(buf));
|
|
371
|
+
});
|
|
372
|
+
process.stdout.write('\n');
|
|
373
|
+
if (typed !== c.name) return console.log('Names did not match — nothing deleted.');
|
|
374
|
+
}
|
|
375
|
+
await api.del(`/apps/${c.id}`);
|
|
376
|
+
console.log(`${c.name} deleted.`);
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
async version() { console.log(`javer-cli ${version}`); },
|
|
380
|
+
async help() { usage(); }
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const lifecycle = async (name, action) => {
|
|
384
|
+
const c = await resolveApp(name);
|
|
385
|
+
await api.post(`/apps/${c.id}/${action}`);
|
|
386
|
+
console.log(`${c.name}: ${action} requested.`);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const usage = () => console.log(`javer ${version} — Javer Webhost from the command line
|
|
390
|
+
|
|
391
|
+
javer ui live dashboard: apps + VMs, one screen
|
|
392
|
+
|
|
393
|
+
javer login [--key <key>] [--url <url>] save an API key (panel.javer.pro/api-keys)
|
|
394
|
+
javer logout forget the saved key locally
|
|
395
|
+
javer whoami plan limits and what is left
|
|
396
|
+
|
|
397
|
+
javer ls list your apps
|
|
398
|
+
javer status <app> details for one app
|
|
399
|
+
javer logs <app> [-f] recent output; -f keeps watching
|
|
400
|
+
javer env <app> [--show] environment variables (hidden by default)
|
|
401
|
+
javer domains <app> custom domains and whether they verified
|
|
402
|
+
|
|
403
|
+
javer start|stop|restart <app> lifecycle
|
|
404
|
+
javer delete <app> [--yes] remove an app, its data and its URL
|
|
405
|
+
|
|
406
|
+
javer vms list your virtual machines
|
|
407
|
+
javer vm <name> details for one VM
|
|
408
|
+
javer vm start|stop|restart <name> VM lifecycle
|
|
409
|
+
|
|
410
|
+
javer db list databases
|
|
411
|
+
javer db create <name> [--engine mysql] new Postgres or MySQL
|
|
412
|
+
|
|
413
|
+
javer deploy deploy the folder you are in
|
|
414
|
+
javer deploy --repo owner/name deploy from a connected GitHub repo
|
|
415
|
+
[--name app] [--branch main] [--ram 256] [--cpu 0.25]
|
|
416
|
+
|
|
417
|
+
javer bird the mascot, in both renderings\n javer version | help
|
|
418
|
+
|
|
419
|
+
Environment:
|
|
420
|
+
JAVER_API_KEY use this key instead of the saved one (for CI)
|
|
421
|
+
JAVER_API_URL point at a different server (for local development)`);
|
|
422
|
+
|
|
423
|
+
// ── entry ──────────────────────────────────────────────────────────────
|
|
424
|
+
(async () => {
|
|
425
|
+
const [, , cmd, ...rest] = process.argv;
|
|
426
|
+
if (!cmd || cmd === '--help' || cmd === '-h') return usage();
|
|
427
|
+
if (cmd === '--version' || cmd === '-v') return commands.version();
|
|
428
|
+
const handler = commands[cmd];
|
|
429
|
+
if (!handler) { console.error(`Unknown command: ${cmd}\n`); usage(); process.exit(1); }
|
|
430
|
+
|
|
431
|
+
const { positional, flags } = parseArgs(rest);
|
|
432
|
+
try {
|
|
433
|
+
await handler(positional, flags);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
// Only rewrite the message when a key was actually sent and refused.
|
|
436
|
+
// Without this check the "not logged in" case — which api.js raises as a
|
|
437
|
+
// synthetic 401 before any request goes out — was reported as "key
|
|
438
|
+
// rejected or expired", sending people to re-check a key they never had.
|
|
439
|
+
if (err instanceof api.ApiError && err.status === 401 && config.apiKey()) {
|
|
440
|
+
die('Key rejected or expired. Run `javer login` again.');
|
|
441
|
+
}
|
|
442
|
+
die(err.message);
|
|
443
|
+
}
|
|
444
|
+
})();
|