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 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
+ })();