@videan/cli 0.1.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/README.md +30 -0
- package/dist/index.js +2114 -0
- package/dist/index.js.map +7 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command17 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/login.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import inquirer from "inquirer";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
|
|
13
|
+
// ../sdk/dist/index.js
|
|
14
|
+
var DEFAULT_API_URL = "https://api.videan.xyz/v1";
|
|
15
|
+
var APP_DOMAIN = process.env.CLOUD_APP_DOMAIN || "videan.xyz";
|
|
16
|
+
var MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024;
|
|
17
|
+
var FARGATE_SIZES = {
|
|
18
|
+
256: { min: 512, max: 2048, step: 512 },
|
|
19
|
+
512: { min: 1024, max: 4096, step: 1024 },
|
|
20
|
+
1024: { min: 2048, max: 8192, step: 1024 },
|
|
21
|
+
2048: { min: 4096, max: 16384, step: 1024 },
|
|
22
|
+
4096: { min: 8192, max: 30720, step: 1024 },
|
|
23
|
+
8192: { min: 16384, max: 61440, step: 4096 },
|
|
24
|
+
16384: { min: 32768, max: 122880, step: 8192 }
|
|
25
|
+
};
|
|
26
|
+
var FARGATE_CPUS = Object.keys(FARGATE_SIZES).map(Number).sort((a, b) => a - b);
|
|
27
|
+
var PLANS = {
|
|
28
|
+
free: {
|
|
29
|
+
id: "free",
|
|
30
|
+
name: "Free",
|
|
31
|
+
baseCents: 0,
|
|
32
|
+
includedUsageCents: 300,
|
|
33
|
+
maxApps: 3,
|
|
34
|
+
maxInstancesPerApp: 1,
|
|
35
|
+
// One app at this size, running all month, is inside the included usage.
|
|
36
|
+
maxCpu: 512,
|
|
37
|
+
maxMemory: 1024,
|
|
38
|
+
maxStorageMb: 1024,
|
|
39
|
+
postgres: true,
|
|
40
|
+
customDomains: false
|
|
41
|
+
},
|
|
42
|
+
pro: {
|
|
43
|
+
id: "pro",
|
|
44
|
+
name: "Pro",
|
|
45
|
+
baseCents: 2e3,
|
|
46
|
+
includedUsageCents: 2e3,
|
|
47
|
+
maxApps: 50,
|
|
48
|
+
maxInstancesPerApp: 4,
|
|
49
|
+
maxCpu: 4096,
|
|
50
|
+
maxMemory: 8192,
|
|
51
|
+
maxStorageMb: 100 * 1024,
|
|
52
|
+
postgres: true,
|
|
53
|
+
customDomains: true
|
|
54
|
+
},
|
|
55
|
+
scale: {
|
|
56
|
+
id: "scale",
|
|
57
|
+
name: "Scale",
|
|
58
|
+
baseCents: 2e4,
|
|
59
|
+
includedUsageCents: 2e4,
|
|
60
|
+
maxApps: 500,
|
|
61
|
+
maxInstancesPerApp: 10,
|
|
62
|
+
maxCpu: 16384,
|
|
63
|
+
maxMemory: 122880,
|
|
64
|
+
maxStorageMb: 1024 * 1024,
|
|
65
|
+
postgres: true,
|
|
66
|
+
customDomains: true
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var SNAKE_KEY = /^[a-z0-9]+(_[a-z0-9]+)+$/;
|
|
70
|
+
var OPAQUE_KEYS = /* @__PURE__ */ new Set(["payload", "files", "metadata"]);
|
|
71
|
+
function camelizeDeep(value) {
|
|
72
|
+
if (Array.isArray(value)) return value.map(camelizeDeep);
|
|
73
|
+
if (value === null || typeof value !== "object") return value;
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [key, v] of Object.entries(value)) {
|
|
76
|
+
const camel = SNAKE_KEY.test(key) ? key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase()) : key;
|
|
77
|
+
out[camel] = OPAQUE_KEYS.has(camel) ? v : camelizeDeep(v);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
var CloudError = class extends Error {
|
|
82
|
+
code;
|
|
83
|
+
status;
|
|
84
|
+
details;
|
|
85
|
+
constructor(message, code, status, details) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = "CloudError";
|
|
88
|
+
this.code = code;
|
|
89
|
+
this.status = status;
|
|
90
|
+
this.details = details;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var CloudClient = class {
|
|
94
|
+
apiKey;
|
|
95
|
+
baseUrl;
|
|
96
|
+
apps;
|
|
97
|
+
secrets;
|
|
98
|
+
orgs;
|
|
99
|
+
apiKeys;
|
|
100
|
+
storage;
|
|
101
|
+
email;
|
|
102
|
+
jobs;
|
|
103
|
+
constructor(options) {
|
|
104
|
+
this.apiKey = options.apiKey;
|
|
105
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
106
|
+
this.apps = new AppsNamespace(this);
|
|
107
|
+
this.secrets = new SecretsNamespace(this);
|
|
108
|
+
this.orgs = new OrgsNamespace(this);
|
|
109
|
+
this.apiKeys = new ApiKeysNamespace(this);
|
|
110
|
+
this.storage = new StorageNamespace(this);
|
|
111
|
+
this.email = new EmailNamespace(this);
|
|
112
|
+
this.jobs = new JobsNamespace(this);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The plan catalogue. Unauthenticated on the API, so this works with any key --
|
|
116
|
+
* including a placeholder one -- and can be called before an account exists to
|
|
117
|
+
* show what an upgrade would allow.
|
|
118
|
+
*/
|
|
119
|
+
async plans() {
|
|
120
|
+
const res = await this.request("GET", "/plans");
|
|
121
|
+
return res.data;
|
|
122
|
+
}
|
|
123
|
+
async request(method, path4, body) {
|
|
124
|
+
const url = `${this.baseUrl}${path4}`;
|
|
125
|
+
const headers = {
|
|
126
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
127
|
+
"Content-Type": "application/json"
|
|
128
|
+
};
|
|
129
|
+
let response;
|
|
130
|
+
try {
|
|
131
|
+
response = await fetch(url, {
|
|
132
|
+
method,
|
|
133
|
+
headers,
|
|
134
|
+
body: body ? JSON.stringify(body) : void 0
|
|
135
|
+
});
|
|
136
|
+
} catch (e) {
|
|
137
|
+
const cause = e?.cause?.code || e?.cause?.message || e?.message || "unknown error";
|
|
138
|
+
throw new CloudError(
|
|
139
|
+
`Could not reach ${url} (${cause}).`,
|
|
140
|
+
"network_error",
|
|
141
|
+
0
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
146
|
+
const err = errorBody.error;
|
|
147
|
+
throw new CloudError(
|
|
148
|
+
err?.message ?? `Request failed with status ${response.status}`,
|
|
149
|
+
err?.code ?? "unknown_error",
|
|
150
|
+
response.status,
|
|
151
|
+
err?.details
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (response.status === 204) {
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
return camelizeDeep(await response.json());
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var AppsNamespace = class {
|
|
161
|
+
constructor(client4) {
|
|
162
|
+
this.client = client4;
|
|
163
|
+
}
|
|
164
|
+
client;
|
|
165
|
+
async create(opts) {
|
|
166
|
+
const body = {
|
|
167
|
+
name: opts.name,
|
|
168
|
+
org_id: opts.orgId,
|
|
169
|
+
postgres_enabled: opts.postgres,
|
|
170
|
+
storage_enabled: opts.storage,
|
|
171
|
+
email_enabled: opts.email,
|
|
172
|
+
auth_required: opts.authRequired,
|
|
173
|
+
visibility: opts.visibility
|
|
174
|
+
};
|
|
175
|
+
const res = await this.client.request("POST", "/apps", body);
|
|
176
|
+
return res.data;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Changes an app's settings.
|
|
180
|
+
*
|
|
181
|
+
* `restartRequired` is true when the change only takes effect on a new
|
|
182
|
+
* container -- size, scale and postgres. The API reports it rather than
|
|
183
|
+
* restarting on its own: a restart is a brief interruption, and doing it as a
|
|
184
|
+
* side effect of a settings write is not the caller's decision to discover
|
|
185
|
+
* afterwards. Follow with `restart()` when it comes back true.
|
|
186
|
+
*/
|
|
187
|
+
async update(appId, changes) {
|
|
188
|
+
const body = {};
|
|
189
|
+
if (changes.name !== void 0) body.name = changes.name;
|
|
190
|
+
if (changes.authRequired !== void 0) body.auth_required = changes.authRequired;
|
|
191
|
+
if (changes.visibility !== void 0) body.visibility = changes.visibility;
|
|
192
|
+
if (changes.status !== void 0) body.status = changes.status;
|
|
193
|
+
if (changes.cpu !== void 0) body.cpu = changes.cpu;
|
|
194
|
+
if (changes.memory !== void 0) body.memory = changes.memory;
|
|
195
|
+
if (changes.instances !== void 0) body.instances = changes.instances;
|
|
196
|
+
if (changes.rateLimitPerMinute !== void 0) {
|
|
197
|
+
body.rate_limit_per_minute = changes.rateLimitPerMinute;
|
|
198
|
+
}
|
|
199
|
+
if (changes.postgres !== void 0) body.postgres_enabled = changes.postgres;
|
|
200
|
+
if (changes.storage !== void 0) body.storage_enabled = changes.storage;
|
|
201
|
+
if (changes.email !== void 0) body.email_enabled = changes.email;
|
|
202
|
+
const res = await this.client.request(
|
|
203
|
+
"PATCH",
|
|
204
|
+
`/apps/${appId}`,
|
|
205
|
+
body
|
|
206
|
+
);
|
|
207
|
+
return res.data;
|
|
208
|
+
}
|
|
209
|
+
async list(orgId) {
|
|
210
|
+
const res = await this.client.request(
|
|
211
|
+
"GET",
|
|
212
|
+
`/apps?org_id=${encodeURIComponent(orgId)}`
|
|
213
|
+
);
|
|
214
|
+
return res.data;
|
|
215
|
+
}
|
|
216
|
+
async get(id) {
|
|
217
|
+
const res = await this.client.request("GET", `/apps/${id}`);
|
|
218
|
+
return res.data;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Deletes the app and tears down its AWS resources.
|
|
222
|
+
*
|
|
223
|
+
* Teardown is fault-isolated per resource, so a partial failure still deletes
|
|
224
|
+
* the app and reports what leaked in `warnings` (HTTP 207) rather than
|
|
225
|
+
* aborting. An empty array means everything was released.
|
|
226
|
+
*/
|
|
227
|
+
async delete(id) {
|
|
228
|
+
const res = await this.client.request("DELETE", `/apps/${id}`);
|
|
229
|
+
return { warnings: res?.warnings ?? [] };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Pushes a snapshot.
|
|
233
|
+
*
|
|
234
|
+
* `files` carries text. Anything that is not valid UTF-8 — an image, a
|
|
235
|
+
* favicon, a compiled binary — must go in `binaryFiles` as base64, because
|
|
236
|
+
* JSON cannot carry arbitrary bytes in a string and a value that is silently
|
|
237
|
+
* re-encoded arrives corrupted. The two are separate maps rather than one
|
|
238
|
+
* with a flag: base64 is not detectable (`"data"` is valid base64 and valid
|
|
239
|
+
* text), so the encoding has to be stated by the caller.
|
|
240
|
+
*/
|
|
241
|
+
async push(appId, files, message, binaryFiles) {
|
|
242
|
+
const res = await this.client.request("POST", `/apps/${appId}/push`, {
|
|
243
|
+
files,
|
|
244
|
+
message,
|
|
245
|
+
// Request bodies are not camelized — only responses are — so the wire
|
|
246
|
+
// name has to be written out here.
|
|
247
|
+
...binaryFiles && Object.keys(binaryFiles).length > 0 ? { binary_files: binaryFiles } : {}
|
|
248
|
+
});
|
|
249
|
+
return res.data;
|
|
250
|
+
}
|
|
251
|
+
async deploy(appId, snapshotId, environment) {
|
|
252
|
+
const res = await this.client.request("POST", `/apps/${appId}/deploy`, {
|
|
253
|
+
snapshot_id: snapshotId,
|
|
254
|
+
environment
|
|
255
|
+
});
|
|
256
|
+
return res.data;
|
|
257
|
+
}
|
|
258
|
+
async promote(deploymentId) {
|
|
259
|
+
const res = await this.client.request("POST", `/deployments/${deploymentId}/promote`, {});
|
|
260
|
+
return res.data;
|
|
261
|
+
}
|
|
262
|
+
async rollback(appId) {
|
|
263
|
+
const res = await this.client.request("POST", `/apps/${appId}/rollback`, {});
|
|
264
|
+
return res.data;
|
|
265
|
+
}
|
|
266
|
+
async checkDeployment(deploymentId) {
|
|
267
|
+
const res = await this.client.request("POST", `/deployments/${deploymentId}/status`, {});
|
|
268
|
+
return res.data;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* A page of the app's logs, newest last.
|
|
272
|
+
*
|
|
273
|
+
* Returns a page rather than a bare array of entries, which is what this used to
|
|
274
|
+
* be: an array cannot say whether there is more to fetch, and the endpoint
|
|
275
|
+
* behind it silently answered with the newest few log streams as though they
|
|
276
|
+
* were the whole app. To page, pass the previous response's `nextToken`; to
|
|
277
|
+
* follow, pass the last event's `timestamp` as `since`.
|
|
278
|
+
*
|
|
279
|
+
* `since`/`until` accept an ISO timestamp, epoch millis, or an offset like
|
|
280
|
+
* `15m`. `q` is a literal substring; `pattern` is CloudWatch filter syntax --
|
|
281
|
+
* pass one or the other, not both.
|
|
282
|
+
*/
|
|
283
|
+
async logs(appId, opts) {
|
|
284
|
+
const params = new URLSearchParams();
|
|
285
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
286
|
+
if (opts?.since) params.set("since", opts.since);
|
|
287
|
+
if (opts?.until) params.set("until", opts.until);
|
|
288
|
+
if (opts?.q) params.set("q", opts.q);
|
|
289
|
+
if (opts?.pattern) params.set("pattern", opts.pattern);
|
|
290
|
+
if (opts?.instance) params.set("instance", opts.instance);
|
|
291
|
+
if (opts?.nextToken) params.set("next_token", opts.nextToken);
|
|
292
|
+
const query = params.toString();
|
|
293
|
+
const path4 = `/apps/${appId}/logs${query ? `?${query}` : ""}`;
|
|
294
|
+
const res = await this.client.request("GET", path4);
|
|
295
|
+
return res.data;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Follows the app's logs, yielding each line as it appears.
|
|
299
|
+
*
|
|
300
|
+
* Polling rather than the API's SSE tail: an async generator works in node
|
|
301
|
+
* without `EventSource`, and it carries the bearer token the same way as every
|
|
302
|
+
* other call. The cursor is the last event's timestamp and the window is
|
|
303
|
+
* reopened *at* it rather than after it -- several lines share a millisecond, so
|
|
304
|
+
* advancing past it would drop the rest of them -- with the boundary
|
|
305
|
+
* millisecond's lines remembered to suppress the repeat.
|
|
306
|
+
*/
|
|
307
|
+
async *followLogs(appId, opts) {
|
|
308
|
+
const interval = opts?.intervalMs ?? 2e3;
|
|
309
|
+
let since = opts?.since ?? "60s";
|
|
310
|
+
let seenAtCursor = /* @__PURE__ */ new Set();
|
|
311
|
+
while (true) {
|
|
312
|
+
const page = await this.logs(appId, {
|
|
313
|
+
since,
|
|
314
|
+
limit: 500,
|
|
315
|
+
q: opts?.q,
|
|
316
|
+
pattern: opts?.pattern,
|
|
317
|
+
instance: opts?.instance
|
|
318
|
+
});
|
|
319
|
+
for (const event of page.events) {
|
|
320
|
+
const key = `${event.stream || ""} ${event.message}`;
|
|
321
|
+
if (event.timestamp === since) {
|
|
322
|
+
if (seenAtCursor.has(key)) continue;
|
|
323
|
+
seenAtCursor.add(key);
|
|
324
|
+
} else {
|
|
325
|
+
since = event.timestamp;
|
|
326
|
+
seenAtCursor = /* @__PURE__ */ new Set([key]);
|
|
327
|
+
}
|
|
328
|
+
yield event;
|
|
329
|
+
}
|
|
330
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Traffic, latency and resource use over a window.
|
|
335
|
+
*
|
|
336
|
+
* `since`/`until` take the same values as `logs`, deliberately: an owner who
|
|
337
|
+
* spots a spike here reads the logs for the same period next.
|
|
338
|
+
*
|
|
339
|
+
* Note the series keys arrive camelized like every other field on this client,
|
|
340
|
+
* so the API's `client_errors` is `clientErrors` and `latency_p95` is
|
|
341
|
+
* `latencyP95`.
|
|
342
|
+
*/
|
|
343
|
+
async metrics(appId, opts) {
|
|
344
|
+
const params = new URLSearchParams();
|
|
345
|
+
if (opts?.since) params.set("since", opts.since);
|
|
346
|
+
if (opts?.until) params.set("until", opts.until);
|
|
347
|
+
const query = params.toString();
|
|
348
|
+
const res = await this.client.request(
|
|
349
|
+
"GET",
|
|
350
|
+
`/apps/${appId}/metrics${query ? `?${query}` : ""}`
|
|
351
|
+
);
|
|
352
|
+
return res.data;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Live runtime state, including the container's exit code when it has crashed.
|
|
356
|
+
* A deployment stays "running" in the database after the task dies, so this is
|
|
357
|
+
* the only way to tell a healthy app from a crash loop.
|
|
358
|
+
*/
|
|
359
|
+
/**
|
|
360
|
+
* What this app cost in a month (`YYYY-MM`, default the month in progress),
|
|
361
|
+
* priced at list rates with no allowance applied -- the plan's included usage
|
|
362
|
+
* belongs to the org, so per-app figures would otherwise sum past the invoice.
|
|
363
|
+
*/
|
|
364
|
+
async usage(appId, period2) {
|
|
365
|
+
const query = period2 ? `?period=${encodeURIComponent(period2)}` : "";
|
|
366
|
+
const res = await this.client.request(
|
|
367
|
+
"GET",
|
|
368
|
+
`/apps/${appId}/usage${query}`
|
|
369
|
+
);
|
|
370
|
+
return res.data;
|
|
371
|
+
}
|
|
372
|
+
async health(appId) {
|
|
373
|
+
const res = await this.client.request("GET", `/apps/${appId}/health`);
|
|
374
|
+
return res.data;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Build output for a deployment. Poll with the previous response's `next_token`
|
|
378
|
+
* to stream a build as it runs.
|
|
379
|
+
*/
|
|
380
|
+
async buildLogs(deploymentId, nextToken) {
|
|
381
|
+
const query = nextToken ? `?next_token=${encodeURIComponent(nextToken)}` : "";
|
|
382
|
+
const res = await this.client.request(
|
|
383
|
+
"GET",
|
|
384
|
+
`/deployments/${deploymentId}/build-logs${query}`
|
|
385
|
+
);
|
|
386
|
+
return res.data;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
var ApiKeysNamespace = class {
|
|
390
|
+
constructor(client4) {
|
|
391
|
+
this.client = client4;
|
|
392
|
+
}
|
|
393
|
+
client;
|
|
394
|
+
/**
|
|
395
|
+
* Creates a key. The plaintext `key` is present on this response only -- the
|
|
396
|
+
* server stores a hash and cannot return it again.
|
|
397
|
+
*/
|
|
398
|
+
async create(opts) {
|
|
399
|
+
const res = await this.client.request("POST", "/api-keys", {
|
|
400
|
+
name: opts.name,
|
|
401
|
+
org_id: opts.orgId,
|
|
402
|
+
expires_in_days: opts.expiresInDays,
|
|
403
|
+
app_id: opts.appId
|
|
404
|
+
});
|
|
405
|
+
return res.data;
|
|
406
|
+
}
|
|
407
|
+
async list(orgId) {
|
|
408
|
+
const res = await this.client.request(
|
|
409
|
+
"GET",
|
|
410
|
+
`/api-keys?org_id=${encodeURIComponent(orgId)}`
|
|
411
|
+
);
|
|
412
|
+
return res.data;
|
|
413
|
+
}
|
|
414
|
+
async revoke(id) {
|
|
415
|
+
await this.client.request("DELETE", `/api-keys/${id}`);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
var SecretsNamespace = class {
|
|
419
|
+
constructor(client4) {
|
|
420
|
+
this.client = client4;
|
|
421
|
+
}
|
|
422
|
+
client;
|
|
423
|
+
async set(appId, key, value) {
|
|
424
|
+
const res = await this.client.request("POST", `/apps/${appId}/secrets`, {
|
|
425
|
+
key,
|
|
426
|
+
value
|
|
427
|
+
});
|
|
428
|
+
return res.data;
|
|
429
|
+
}
|
|
430
|
+
async list(appId) {
|
|
431
|
+
const res = await this.client.request("GET", `/apps/${appId}/secrets`);
|
|
432
|
+
return res.data;
|
|
433
|
+
}
|
|
434
|
+
async delete(appId, key) {
|
|
435
|
+
await this.client.request("DELETE", `/apps/${appId}/secrets/${key}`);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
var StorageNamespace = class {
|
|
439
|
+
constructor(client4) {
|
|
440
|
+
this.client = client4;
|
|
441
|
+
}
|
|
442
|
+
client;
|
|
443
|
+
async list(appId, opts = {}) {
|
|
444
|
+
const q = new URLSearchParams();
|
|
445
|
+
if (opts.prefix) q.set("prefix", opts.prefix);
|
|
446
|
+
if (opts.limit) q.set("limit", String(opts.limit));
|
|
447
|
+
if (opts.cursor) q.set("cursor", opts.cursor);
|
|
448
|
+
const query = q.toString() ? `?${q}` : "";
|
|
449
|
+
const res = await this.client.request("GET", `/apps/${appId}/storage/objects${query}`);
|
|
450
|
+
return res.data;
|
|
451
|
+
}
|
|
452
|
+
/** Every object under the prefix, paged through for you. */
|
|
453
|
+
async listAll(appId, prefix) {
|
|
454
|
+
const all = [];
|
|
455
|
+
let cursor;
|
|
456
|
+
do {
|
|
457
|
+
const page = await this.list(appId, { prefix, cursor, limit: 1e3 });
|
|
458
|
+
all.push(...page.objects);
|
|
459
|
+
cursor = page.nextCursor ?? void 0;
|
|
460
|
+
} while (cursor);
|
|
461
|
+
return all;
|
|
462
|
+
}
|
|
463
|
+
async usage(appId) {
|
|
464
|
+
const res = await this.client.request(
|
|
465
|
+
"GET",
|
|
466
|
+
`/apps/${appId}/storage/usage`
|
|
467
|
+
);
|
|
468
|
+
return res.data;
|
|
469
|
+
}
|
|
470
|
+
/** Metadata for one object, or null when it is not there. */
|
|
471
|
+
async head(appId, key) {
|
|
472
|
+
try {
|
|
473
|
+
const res = await this.client.request(
|
|
474
|
+
"GET",
|
|
475
|
+
`/apps/${appId}/storage/object?key=${encodeURIComponent(key)}`
|
|
476
|
+
);
|
|
477
|
+
return res.data;
|
|
478
|
+
} catch (e) {
|
|
479
|
+
if (e instanceof CloudError && e.status === 404) return null;
|
|
480
|
+
throw e;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async delete(appId, key) {
|
|
484
|
+
await this.client.request(
|
|
485
|
+
"DELETE",
|
|
486
|
+
`/apps/${appId}/storage/object?key=${encodeURIComponent(key)}`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* A short-lived URL for one key and one method. Hand a `put` URL to a browser to
|
|
491
|
+
* upload without the bytes touching the app, or a `get` URL to share a private
|
|
492
|
+
* object for a few minutes. Treat it as a credential: anyone holding it has that
|
|
493
|
+
* access until it expires.
|
|
494
|
+
*/
|
|
495
|
+
async presign(appId, key, opts = {}) {
|
|
496
|
+
const res = await this.client.request("POST", `/apps/${appId}/storage/presign`, {
|
|
497
|
+
key,
|
|
498
|
+
method: opts.method ?? "get",
|
|
499
|
+
expires_in: opts.expiresIn,
|
|
500
|
+
content_type: opts.contentType,
|
|
501
|
+
download: opts.download
|
|
502
|
+
});
|
|
503
|
+
return res.data;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Presigns and uploads in one call.
|
|
507
|
+
*
|
|
508
|
+
* `contentType` is signed into the URL, so it is sent on the PUT as well --
|
|
509
|
+
* omitting it there would make S3 reject the signature with an opaque 403.
|
|
510
|
+
*/
|
|
511
|
+
async upload(appId, key, body, opts = {}) {
|
|
512
|
+
const signed = await this.presign(appId, key, {
|
|
513
|
+
method: "put",
|
|
514
|
+
contentType: opts.contentType
|
|
515
|
+
});
|
|
516
|
+
const res = await fetch(signed.url, {
|
|
517
|
+
method: "PUT",
|
|
518
|
+
headers: opts.contentType ? { "Content-Type": opts.contentType } : void 0,
|
|
519
|
+
body
|
|
520
|
+
});
|
|
521
|
+
if (!res.ok) {
|
|
522
|
+
throw new CloudError(
|
|
523
|
+
`Upload failed with status ${res.status}: ${(await res.text()).slice(0, 300)}`,
|
|
524
|
+
"upload_failed",
|
|
525
|
+
res.status
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return { key, size: 0, lastModified: null, etag: res.headers.get("etag") };
|
|
529
|
+
}
|
|
530
|
+
/** Presigns and fetches. Returns the raw bytes; decode as the caller sees fit. */
|
|
531
|
+
async download(appId, key) {
|
|
532
|
+
const signed = await this.presign(appId, key, { method: "get" });
|
|
533
|
+
const res = await fetch(signed.url);
|
|
534
|
+
if (!res.ok) {
|
|
535
|
+
throw new CloudError(
|
|
536
|
+
`Download failed with status ${res.status}`,
|
|
537
|
+
res.status === 404 ? "not_found" : "download_failed",
|
|
538
|
+
res.status
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
var EmailNamespace = class {
|
|
545
|
+
constructor(client4) {
|
|
546
|
+
this.client = client4;
|
|
547
|
+
}
|
|
548
|
+
client;
|
|
549
|
+
/**
|
|
550
|
+
* Sends one message, to up to 50 recipients, as a single all-or-nothing send.
|
|
551
|
+
*
|
|
552
|
+
* Throws `CloudError` with status 429 once the app's daily quota is used up, and
|
|
553
|
+
* 502 when the provider rejected the message -- the message text carries the
|
|
554
|
+
* reason, which while the sending account is sandboxed is usually that the
|
|
555
|
+
* recipient address is not verified.
|
|
556
|
+
*/
|
|
557
|
+
async send(appId, message) {
|
|
558
|
+
const res = await this.client.request(
|
|
559
|
+
"POST",
|
|
560
|
+
`/apps/${appId}/email`,
|
|
561
|
+
{
|
|
562
|
+
to: message.to,
|
|
563
|
+
subject: message.subject,
|
|
564
|
+
text: message.text,
|
|
565
|
+
html: message.html,
|
|
566
|
+
reply_to: message.replyTo
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
return res.data;
|
|
570
|
+
}
|
|
571
|
+
/** Sends used today and what is left. Resets at UTC midnight. */
|
|
572
|
+
async quota(appId) {
|
|
573
|
+
const res = await this.client.request(
|
|
574
|
+
"GET",
|
|
575
|
+
`/apps/${appId}/email/quota`
|
|
576
|
+
);
|
|
577
|
+
return res.data;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
var JobsNamespace = class {
|
|
581
|
+
constructor(client4) {
|
|
582
|
+
this.client = client4;
|
|
583
|
+
}
|
|
584
|
+
client;
|
|
585
|
+
/**
|
|
586
|
+
* Enqueues a job.
|
|
587
|
+
*
|
|
588
|
+
* Pass `idempotencyKey` when the caller may retry this enqueue: a repeat returns
|
|
589
|
+
* the job already queued, with `deduplicated: true`, instead of a second copy of
|
|
590
|
+
* the work.
|
|
591
|
+
*/
|
|
592
|
+
async enqueue(appId, options = {}) {
|
|
593
|
+
const res = await this.client.request("POST", `/apps/${appId}/jobs`, {
|
|
594
|
+
queue: options.queue,
|
|
595
|
+
path: options.path,
|
|
596
|
+
method: options.method,
|
|
597
|
+
payload: options.payload,
|
|
598
|
+
max_attempts: options.maxAttempts,
|
|
599
|
+
timeout_seconds: options.timeoutSeconds,
|
|
600
|
+
delay_seconds: options.delaySeconds,
|
|
601
|
+
idempotency_key: options.idempotencyKey
|
|
602
|
+
});
|
|
603
|
+
return res.data;
|
|
604
|
+
}
|
|
605
|
+
/** A page of jobs, newest first, with the queue depth per status alongside. */
|
|
606
|
+
async list(appId, options = {}) {
|
|
607
|
+
const params = new URLSearchParams();
|
|
608
|
+
if (options.status) params.set("status", options.status);
|
|
609
|
+
if (options.queue) params.set("queue", options.queue);
|
|
610
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
611
|
+
const query = params.toString();
|
|
612
|
+
const res = await this.client.request(
|
|
613
|
+
"GET",
|
|
614
|
+
`/apps/${appId}/jobs${query ? `?${query}` : ""}`
|
|
615
|
+
);
|
|
616
|
+
return res.data;
|
|
617
|
+
}
|
|
618
|
+
async get(appId, jobId) {
|
|
619
|
+
const res = await this.client.request("GET", `/apps/${appId}/jobs/${jobId}`);
|
|
620
|
+
return res.data;
|
|
621
|
+
}
|
|
622
|
+
/** Runs a dead job again, from a clean attempt count. */
|
|
623
|
+
async retry(appId, jobId) {
|
|
624
|
+
const res = await this.client.request(
|
|
625
|
+
"POST",
|
|
626
|
+
`/apps/${appId}/jobs/${jobId}/retry`
|
|
627
|
+
);
|
|
628
|
+
return res.data;
|
|
629
|
+
}
|
|
630
|
+
/** Cancels a job that has not started. Throws 409 once it is running. */
|
|
631
|
+
async cancel(appId, jobId) {
|
|
632
|
+
const res = await this.client.request(
|
|
633
|
+
"POST",
|
|
634
|
+
`/apps/${appId}/jobs/${jobId}/cancel`
|
|
635
|
+
);
|
|
636
|
+
return res.data;
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
var OrgsNamespace = class {
|
|
640
|
+
constructor(client4) {
|
|
641
|
+
this.client = client4;
|
|
642
|
+
}
|
|
643
|
+
client;
|
|
644
|
+
async list() {
|
|
645
|
+
const res = await this.client.request("GET", "/orgs");
|
|
646
|
+
return res.data;
|
|
647
|
+
}
|
|
648
|
+
async create(name) {
|
|
649
|
+
const res = await this.client.request("POST", "/orgs", { name });
|
|
650
|
+
return res.data;
|
|
651
|
+
}
|
|
652
|
+
async get(id) {
|
|
653
|
+
const res = await this.client.request("GET", `/orgs/${id}`);
|
|
654
|
+
return res.data;
|
|
655
|
+
}
|
|
656
|
+
async members(id) {
|
|
657
|
+
const res = await this.client.request("GET", `/orgs/${id}/members`);
|
|
658
|
+
return res.data;
|
|
659
|
+
}
|
|
660
|
+
async invite(id, email, role = "member") {
|
|
661
|
+
await this.client.request("POST", `/orgs/${id}/members`, { email, role });
|
|
662
|
+
}
|
|
663
|
+
async removeMember(id, userId) {
|
|
664
|
+
await this.client.request("DELETE", `/orgs/${id}/members/${userId}`);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Metered usage and the priced invoice for a month (`YYYY-MM`, default the month
|
|
668
|
+
* in progress). Read-only: nothing in the SDK changes a plan or takes a payment.
|
|
669
|
+
*/
|
|
670
|
+
async usage(id, period2) {
|
|
671
|
+
const query = period2 ? `?period=${encodeURIComponent(period2)}` : "";
|
|
672
|
+
const res = await this.client.request(
|
|
673
|
+
"GET",
|
|
674
|
+
`/orgs/${id}/usage${query}`
|
|
675
|
+
);
|
|
676
|
+
return res.data;
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// ../shared/src/constants.ts
|
|
681
|
+
var SUPPORTED_RUNTIMES = [
|
|
682
|
+
"node-20",
|
|
683
|
+
"node-22",
|
|
684
|
+
"python-3.11",
|
|
685
|
+
"python-3.12",
|
|
686
|
+
"bun-1",
|
|
687
|
+
"deno-2"
|
|
688
|
+
];
|
|
689
|
+
var DEFAULT_API_URL2 = "https://api.videan.xyz/v1";
|
|
690
|
+
var APP_DOMAIN2 = process.env.CLOUD_APP_DOMAIN || "videan.xyz";
|
|
691
|
+
function appUrl(slug) {
|
|
692
|
+
return `https://${slug}.${APP_DOMAIN2}`;
|
|
693
|
+
}
|
|
694
|
+
var CONFIG_FILE_NAME = "cloud.yaml";
|
|
695
|
+
var MAX_FILE_SIZE_BYTES2 = 50 * 1024 * 1024;
|
|
696
|
+
|
|
697
|
+
// ../shared/src/sizing.ts
|
|
698
|
+
var FARGATE_SIZES2 = {
|
|
699
|
+
256: { min: 512, max: 2048, step: 512 },
|
|
700
|
+
512: { min: 1024, max: 4096, step: 1024 },
|
|
701
|
+
1024: { min: 2048, max: 8192, step: 1024 },
|
|
702
|
+
2048: { min: 4096, max: 16384, step: 1024 },
|
|
703
|
+
4096: { min: 8192, max: 30720, step: 1024 },
|
|
704
|
+
8192: { min: 16384, max: 61440, step: 4096 },
|
|
705
|
+
16384: { min: 32768, max: 122880, step: 8192 }
|
|
706
|
+
};
|
|
707
|
+
var FARGATE_CPUS2 = Object.keys(FARGATE_SIZES2).map(Number).sort((a, b) => a - b);
|
|
708
|
+
|
|
709
|
+
// ../shared/src/pricing.ts
|
|
710
|
+
var PLANS2 = {
|
|
711
|
+
free: {
|
|
712
|
+
id: "free",
|
|
713
|
+
name: "Free",
|
|
714
|
+
baseCents: 0,
|
|
715
|
+
includedUsageCents: 300,
|
|
716
|
+
maxApps: 3,
|
|
717
|
+
maxInstancesPerApp: 1,
|
|
718
|
+
// One app at this size, running all month, is inside the included usage.
|
|
719
|
+
maxCpu: 512,
|
|
720
|
+
maxMemory: 1024,
|
|
721
|
+
maxStorageMb: 1024,
|
|
722
|
+
postgres: true,
|
|
723
|
+
customDomains: false
|
|
724
|
+
},
|
|
725
|
+
pro: {
|
|
726
|
+
id: "pro",
|
|
727
|
+
name: "Pro",
|
|
728
|
+
baseCents: 2e3,
|
|
729
|
+
includedUsageCents: 2e3,
|
|
730
|
+
maxApps: 50,
|
|
731
|
+
maxInstancesPerApp: 4,
|
|
732
|
+
maxCpu: 4096,
|
|
733
|
+
maxMemory: 8192,
|
|
734
|
+
maxStorageMb: 100 * 1024,
|
|
735
|
+
postgres: true,
|
|
736
|
+
customDomains: true
|
|
737
|
+
},
|
|
738
|
+
scale: {
|
|
739
|
+
id: "scale",
|
|
740
|
+
name: "Scale",
|
|
741
|
+
baseCents: 2e4,
|
|
742
|
+
includedUsageCents: 2e4,
|
|
743
|
+
maxApps: 500,
|
|
744
|
+
maxInstancesPerApp: 10,
|
|
745
|
+
maxCpu: 16384,
|
|
746
|
+
maxMemory: 122880,
|
|
747
|
+
maxStorageMb: 1024 * 1024,
|
|
748
|
+
postgres: true,
|
|
749
|
+
customDomains: true
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// ../shared/src/visibility.ts
|
|
754
|
+
var VISIBILITIES = ["private", "org", "authenticated", "public"];
|
|
755
|
+
var DEFAULT_VISIBILITY = "org";
|
|
756
|
+
function isVisibility(value) {
|
|
757
|
+
return typeof value === "string" && VISIBILITIES.includes(value);
|
|
758
|
+
}
|
|
759
|
+
function visibilityOf(value) {
|
|
760
|
+
return isVisibility(value) ? value : "private";
|
|
761
|
+
}
|
|
762
|
+
var VISIBILITY_LABELS = {
|
|
763
|
+
private: "Only people with access",
|
|
764
|
+
org: "Anyone in the organization",
|
|
765
|
+
authenticated: "Anyone signed in to Videan",
|
|
766
|
+
public: "Anyone with the link"
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
// src/config.ts
|
|
770
|
+
import Conf from "conf";
|
|
771
|
+
var config = new Conf({
|
|
772
|
+
projectName: "cloud-cli",
|
|
773
|
+
schema: {
|
|
774
|
+
apiKey: { type: "string" },
|
|
775
|
+
defaultOrg: { type: "string" },
|
|
776
|
+
currentApp: { type: "string" },
|
|
777
|
+
baseUrl: { type: "string" }
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
function fromEnv(name) {
|
|
781
|
+
const value = process.env[`VIDEAN_${name}`] || process.env[`CLOUD_${name}`];
|
|
782
|
+
return value ? value : void 0;
|
|
783
|
+
}
|
|
784
|
+
function getApiKey() {
|
|
785
|
+
return fromEnv("API_KEY") || config.get("apiKey");
|
|
786
|
+
}
|
|
787
|
+
function setApiKey(key) {
|
|
788
|
+
config.set("apiKey", key);
|
|
789
|
+
}
|
|
790
|
+
function getDefaultOrg() {
|
|
791
|
+
return fromEnv("ORG") || config.get("defaultOrg");
|
|
792
|
+
}
|
|
793
|
+
function setDefaultOrg(org) {
|
|
794
|
+
config.set("defaultOrg", org);
|
|
795
|
+
}
|
|
796
|
+
function getCurrentApp() {
|
|
797
|
+
return fromEnv("APP") || config.get("currentApp");
|
|
798
|
+
}
|
|
799
|
+
function setCurrentApp(appId) {
|
|
800
|
+
config.set("currentApp", appId);
|
|
801
|
+
}
|
|
802
|
+
function getBaseUrl() {
|
|
803
|
+
return fromEnv("API_URL") || config.get("baseUrl");
|
|
804
|
+
}
|
|
805
|
+
function setBaseUrl(url) {
|
|
806
|
+
config.set("baseUrl", url);
|
|
807
|
+
}
|
|
808
|
+
function configPath() {
|
|
809
|
+
return config.path;
|
|
810
|
+
}
|
|
811
|
+
function clearConfig() {
|
|
812
|
+
config.clear();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/commands/login.ts
|
|
816
|
+
var loginCommand = new Command("login").description("Log in to Cloud").option("-e, --email <email>", "Email (skips the prompt)").option("-p, --password <password>", "Password (skips the prompt; prefer VIDEAN_PASSWORD)").option("-o, --org <id>", "Organization to scope the key to (skips the picker)").option("--api-url <url>", "API base URL to use and remember (default: the public API)").action(async (opts) => {
|
|
817
|
+
const password = opts.password || process.env.VIDEAN_PASSWORD || process.env.CLOUD_PASSWORD;
|
|
818
|
+
if ((!opts.email || !password) && !process.stdin.isTTY) {
|
|
819
|
+
console.error(
|
|
820
|
+
chalk.red(
|
|
821
|
+
"login needs a terminal to prompt. Non-interactively, pass --email and --password (or VIDEAN_PASSWORD), or skip login entirely by setting VIDEAN_API_KEY."
|
|
822
|
+
)
|
|
823
|
+
);
|
|
824
|
+
process.exit(1);
|
|
825
|
+
}
|
|
826
|
+
const prompts = [
|
|
827
|
+
...opts.email ? [] : [{ type: "input", name: "email", message: "Email:" }],
|
|
828
|
+
...password ? [] : [{ type: "password", name: "password", message: "Password:" }]
|
|
829
|
+
];
|
|
830
|
+
const answers = {
|
|
831
|
+
email: opts.email,
|
|
832
|
+
password,
|
|
833
|
+
...prompts.length ? await inquirer.prompt(prompts) : {}
|
|
834
|
+
};
|
|
835
|
+
if (opts.apiUrl) setBaseUrl(opts.apiUrl.replace(/\/$/, ""));
|
|
836
|
+
const baseUrl = (getBaseUrl() || DEFAULT_API_URL2).replace(/\/$/, "");
|
|
837
|
+
const spinner = ora("Logging in...").start();
|
|
838
|
+
let token;
|
|
839
|
+
try {
|
|
840
|
+
const response = await fetch(`${baseUrl}/auth/login`, {
|
|
841
|
+
method: "POST",
|
|
842
|
+
headers: { "Content-Type": "application/json" },
|
|
843
|
+
body: JSON.stringify({ email: answers.email, password: answers.password })
|
|
844
|
+
});
|
|
845
|
+
if (!response.ok) {
|
|
846
|
+
if (response.status === 401) {
|
|
847
|
+
spinner.fail("Login failed. Check your credentials.");
|
|
848
|
+
} else {
|
|
849
|
+
spinner.fail(`Login failed: ${response.status} from ${baseUrl}/auth/login`);
|
|
850
|
+
console.error(
|
|
851
|
+
chalk.gray(" Wrong API URL? Re-run with --api-url <url>, or `videan logout` to reset.")
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
const data = await response.json();
|
|
857
|
+
token = data.data.token;
|
|
858
|
+
} catch (e) {
|
|
859
|
+
spinner.fail(`Login failed: could not reach ${baseUrl} (${e?.message || e})`);
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
const session = new CloudClient({ apiKey: token, baseUrl });
|
|
863
|
+
let orgId;
|
|
864
|
+
try {
|
|
865
|
+
const orgs = await session.orgs.list();
|
|
866
|
+
if (opts.org) {
|
|
867
|
+
const match = orgs.find((o) => o.id === opts.org || o.name === opts.org);
|
|
868
|
+
if (!match) {
|
|
869
|
+
spinner.fail(`No organization ${opts.org} for this account.`);
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
orgId = match.id;
|
|
873
|
+
} else if (orgs.length > 1 && !process.stdin.isTTY) {
|
|
874
|
+
spinner.fail(
|
|
875
|
+
`Several organizations (${orgs.map((o) => o.id).join(", ")}). Pass --org <id>.`
|
|
876
|
+
);
|
|
877
|
+
process.exit(1);
|
|
878
|
+
} else if (orgs.length > 1) {
|
|
879
|
+
spinner.stop();
|
|
880
|
+
const pick = await inquirer.prompt([
|
|
881
|
+
{
|
|
882
|
+
type: "list",
|
|
883
|
+
name: "orgId",
|
|
884
|
+
message: "Organization:",
|
|
885
|
+
choices: orgs.map((o) => ({ name: `${o.name} (${o.id})`, value: o.id }))
|
|
886
|
+
}
|
|
887
|
+
]);
|
|
888
|
+
orgId = pick.orgId;
|
|
889
|
+
spinner.start("Creating CLI key...");
|
|
890
|
+
} else {
|
|
891
|
+
orgId = orgs[0]?.id;
|
|
892
|
+
}
|
|
893
|
+
} catch (e) {
|
|
894
|
+
spinner.fail(`Logged in, but could not list organizations: ${e?.message}`);
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
897
|
+
if (!orgId) {
|
|
898
|
+
setApiKey(token);
|
|
899
|
+
spinner.warn(
|
|
900
|
+
chalk.yellow("Logged in, but you have no organization yet. Run `videan orgs create <name>`.")
|
|
901
|
+
);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
try {
|
|
905
|
+
const key = await session.apiKeys.create({
|
|
906
|
+
name: `cli@${os.hostname()}`,
|
|
907
|
+
orgId
|
|
908
|
+
});
|
|
909
|
+
setApiKey(key.key);
|
|
910
|
+
setDefaultOrg(orgId);
|
|
911
|
+
spinner.succeed(chalk.green("Logged in."));
|
|
912
|
+
console.log(chalk.gray(` Org: ${orgId}`));
|
|
913
|
+
console.log(chalk.gray(` Key: ${key.keyPrefix}\u2026 (revoke with \`videan keys revoke ${key.id}\`)`));
|
|
914
|
+
} catch (e) {
|
|
915
|
+
setApiKey(token);
|
|
916
|
+
setDefaultOrg(orgId);
|
|
917
|
+
spinner.warn(
|
|
918
|
+
chalk.yellow(`Logged in with a temporary session (key creation failed: ${e?.message}).`)
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// src/commands/logout.ts
|
|
924
|
+
import { Command as Command2 } from "commander";
|
|
925
|
+
import chalk2 from "chalk";
|
|
926
|
+
var logoutCommand = new Command2("logout").description("Forget the saved key, org, app and API URL").action(() => {
|
|
927
|
+
const where = configPath();
|
|
928
|
+
clearConfig();
|
|
929
|
+
console.log(chalk2.green("Logged out."));
|
|
930
|
+
console.log(chalk2.gray(` Cleared ${where}`));
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// src/commands/init.ts
|
|
934
|
+
import { Command as Command3 } from "commander";
|
|
935
|
+
import inquirer2 from "inquirer";
|
|
936
|
+
import chalk3 from "chalk";
|
|
937
|
+
import fs from "fs-extra";
|
|
938
|
+
import path from "node:path";
|
|
939
|
+
var initCommand = new Command3("init").description("Initialize a new Cloud app in the current directory").action(async () => {
|
|
940
|
+
const configPath2 = path.join(process.cwd(), CONFIG_FILE_NAME);
|
|
941
|
+
if (await fs.pathExists(configPath2)) {
|
|
942
|
+
console.log(chalk3.yellow(`${CONFIG_FILE_NAME} already exists in this directory.`));
|
|
943
|
+
const { overwrite } = await inquirer2.prompt([
|
|
944
|
+
{ type: "confirm", name: "overwrite", message: "Overwrite?", default: false }
|
|
945
|
+
]);
|
|
946
|
+
if (!overwrite) return;
|
|
947
|
+
}
|
|
948
|
+
const answers = await inquirer2.prompt([
|
|
949
|
+
{ type: "input", name: "name", message: "App name:", default: path.basename(process.cwd()) },
|
|
950
|
+
{
|
|
951
|
+
type: "list",
|
|
952
|
+
name: "runtime",
|
|
953
|
+
message: "Runtime:",
|
|
954
|
+
choices: SUPPORTED_RUNTIMES
|
|
955
|
+
},
|
|
956
|
+
{ type: "input", name: "entrypoint", message: "Entrypoint:", default: "index.ts" }
|
|
957
|
+
]);
|
|
958
|
+
const config2 = [
|
|
959
|
+
`name: ${answers.name}`,
|
|
960
|
+
`runtime: ${answers.runtime}`,
|
|
961
|
+
`entrypoint: ${answers.entrypoint}`,
|
|
962
|
+
""
|
|
963
|
+
].join("\n");
|
|
964
|
+
await fs.writeFile(configPath2, config2, "utf-8");
|
|
965
|
+
console.log(chalk3.green(`Created ${CONFIG_FILE_NAME}`));
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// src/commands/deploy.ts
|
|
969
|
+
import { Command as Command4 } from "commander";
|
|
970
|
+
import chalk5 from "chalk";
|
|
971
|
+
import ora2 from "ora";
|
|
972
|
+
|
|
973
|
+
// src/util.ts
|
|
974
|
+
import fs2 from "fs-extra";
|
|
975
|
+
import path2 from "node:path";
|
|
976
|
+
import ignore from "ignore";
|
|
977
|
+
import chalk4 from "chalk";
|
|
978
|
+
var MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
979
|
+
async function collectFiles(dir) {
|
|
980
|
+
const ig = ignore();
|
|
981
|
+
ig.add([
|
|
982
|
+
".git",
|
|
983
|
+
"node_modules",
|
|
984
|
+
".env",
|
|
985
|
+
".env.*",
|
|
986
|
+
".next",
|
|
987
|
+
".turbo",
|
|
988
|
+
"coverage",
|
|
989
|
+
".DS_Store",
|
|
990
|
+
"*.log"
|
|
991
|
+
]);
|
|
992
|
+
const gitignorePath = path2.join(dir, ".gitignore");
|
|
993
|
+
if (await fs2.pathExists(gitignorePath)) {
|
|
994
|
+
const content = await fs2.readFile(gitignorePath, "utf-8");
|
|
995
|
+
ig.add(content);
|
|
996
|
+
}
|
|
997
|
+
const cloudignorePath = path2.join(dir, ".cloudignore");
|
|
998
|
+
if (await fs2.pathExists(cloudignorePath)) {
|
|
999
|
+
const content = await fs2.readFile(cloudignorePath, "utf-8");
|
|
1000
|
+
ig.add(content);
|
|
1001
|
+
}
|
|
1002
|
+
const files = {};
|
|
1003
|
+
const binaryFiles = {};
|
|
1004
|
+
const skipped = [];
|
|
1005
|
+
async function walk(currentDir) {
|
|
1006
|
+
const entries = await fs2.readdir(currentDir, { withFileTypes: true });
|
|
1007
|
+
for (const entry of entries) {
|
|
1008
|
+
const fullPath = path2.join(currentDir, entry.name);
|
|
1009
|
+
const relativePath = path2.relative(dir, fullPath);
|
|
1010
|
+
const ignorePath = entry.isDirectory() ? `${relativePath}/` : relativePath;
|
|
1011
|
+
if (ig.ignores(ignorePath)) continue;
|
|
1012
|
+
if (entry.isDirectory()) {
|
|
1013
|
+
await walk(fullPath);
|
|
1014
|
+
} else if (entry.isFile()) {
|
|
1015
|
+
const buffer = await fs2.readFile(fullPath);
|
|
1016
|
+
if (buffer.byteLength > MAX_FILE_BYTES) {
|
|
1017
|
+
skipped.push({
|
|
1018
|
+
path: relativePath,
|
|
1019
|
+
reason: `${Math.round(buffer.byteLength / 1024 / 1024)}MB, over the ${MAX_FILE_BYTES / 1024 / 1024}MB limit`
|
|
1020
|
+
});
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
const text = buffer.toString("utf-8");
|
|
1024
|
+
if (Buffer.from(text, "utf-8").equals(buffer)) {
|
|
1025
|
+
files[relativePath] = text;
|
|
1026
|
+
} else {
|
|
1027
|
+
binaryFiles[relativePath] = buffer.toString("base64");
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
await walk(dir);
|
|
1033
|
+
return { files, binaryFiles, skipped };
|
|
1034
|
+
}
|
|
1035
|
+
function formatStatus(status) {
|
|
1036
|
+
switch (status) {
|
|
1037
|
+
case "running":
|
|
1038
|
+
return chalk4.green(status);
|
|
1039
|
+
case "building":
|
|
1040
|
+
case "deploying":
|
|
1041
|
+
return chalk4.yellow(status);
|
|
1042
|
+
case "failed":
|
|
1043
|
+
case "crashed":
|
|
1044
|
+
return chalk4.red(status);
|
|
1045
|
+
case "stopped":
|
|
1046
|
+
return chalk4.gray(status);
|
|
1047
|
+
default:
|
|
1048
|
+
return status;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function formatRow(columns, widths) {
|
|
1052
|
+
return columns.map((col, i) => col.padEnd(widths[i] || 20)).join(" ");
|
|
1053
|
+
}
|
|
1054
|
+
function requireApiKey(apiKey) {
|
|
1055
|
+
if (!apiKey) {
|
|
1056
|
+
console.error(chalk4.red("Not logged in. Run `videan login` first."));
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function requireOrg(orgId) {
|
|
1061
|
+
if (!orgId) {
|
|
1062
|
+
console.error(chalk4.red("No organization. Pass --org <id> or run `videan login` again."));
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
return orgId;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/commands/deploy.ts
|
|
1069
|
+
var deployCommand = new Command4("deploy").description("Deploy the current directory").option("-a, --app <id>", "App ID to deploy to").option("-m, --message <msg>", "Deployment message").option("--env <environment>", "preview or production (default: production)").action(async (opts) => {
|
|
1070
|
+
const apiKey = getApiKey();
|
|
1071
|
+
requireApiKey(apiKey);
|
|
1072
|
+
const appId = opts.app || getCurrentApp();
|
|
1073
|
+
if (!appId) {
|
|
1074
|
+
console.error(chalk5.red("No app specified. Use --app or run `videan init` first."));
|
|
1075
|
+
process.exit(1);
|
|
1076
|
+
}
|
|
1077
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1078
|
+
const spinner = ora2("Collecting files...").start();
|
|
1079
|
+
const { files, binaryFiles, skipped } = await collectFiles(process.cwd());
|
|
1080
|
+
const binaryCount = Object.keys(binaryFiles).length;
|
|
1081
|
+
const fileCount = Object.keys(files).length + binaryCount;
|
|
1082
|
+
if (!fileCount) {
|
|
1083
|
+
spinner.fail(chalk5.red(`Nothing to deploy: no files under ${process.cwd()}.`));
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
if (skipped.length) {
|
|
1087
|
+
spinner.warn(chalk5.yellow(`Skipped ${skipped.length} file(s):`));
|
|
1088
|
+
for (const file of skipped) console.log(chalk5.gray(` ${file.path} \u2014 ${file.reason}`));
|
|
1089
|
+
spinner.start();
|
|
1090
|
+
}
|
|
1091
|
+
spinner.text = `Pushing ${fileCount} files${binaryCount ? ` (${binaryCount} binary)` : ""}...`;
|
|
1092
|
+
const snapshot = await client4.apps.push(appId, files, opts.message, binaryFiles);
|
|
1093
|
+
spinner.text = "Triggering deployment...";
|
|
1094
|
+
if (opts.env && opts.env !== "preview" && opts.env !== "production") {
|
|
1095
|
+
spinner.fail(chalk5.red(`--env must be preview or production, not ${opts.env}`));
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
const deployment = await client4.apps.deploy(appId, snapshot.id, opts.env);
|
|
1099
|
+
spinner.text = `Deploying (${deployment.status})...`;
|
|
1100
|
+
let current = deployment;
|
|
1101
|
+
let consecutiveErrors = 0;
|
|
1102
|
+
let lastError = "";
|
|
1103
|
+
while (current.status === "building" || current.status === "deploying") {
|
|
1104
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
1105
|
+
const elapsed = Math.round((Date.now() - Date.parse(current.createdAt)) / 1e3);
|
|
1106
|
+
spinner.text = `${current.status === "building" ? "Building" : "Starting"}... (${elapsed}s)`;
|
|
1107
|
+
try {
|
|
1108
|
+
current = await client4.apps.checkDeployment(deployment.id);
|
|
1109
|
+
consecutiveErrors = 0;
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
lastError = e?.message || String(e);
|
|
1112
|
+
if (++consecutiveErrors >= 5) {
|
|
1113
|
+
spinner.fail(chalk5.red(`Lost track of the deployment: ${lastError}`));
|
|
1114
|
+
console.log(chalk5.gray(` It may still finish. Check with \`videan status\`.`));
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (current.status === "running") {
|
|
1120
|
+
spinner.succeed(chalk5.green(`Deployed successfully!`));
|
|
1121
|
+
if (current.url) {
|
|
1122
|
+
const url = /^https?:\/\//.test(current.url) ? current.url : `https://${current.url}`;
|
|
1123
|
+
console.log(chalk5.cyan(` URL: ${url}`));
|
|
1124
|
+
}
|
|
1125
|
+
if (current.previewUrl) {
|
|
1126
|
+
console.log(chalk5.gray(` This version: ${current.previewUrl}`));
|
|
1127
|
+
}
|
|
1128
|
+
} else if (current.status === "failed" || current.status === "crashed") {
|
|
1129
|
+
spinner.fail(chalk5.red(`Deployment ${current.status}: ${current.error || "unknown error"}`));
|
|
1130
|
+
console.log(chalk5.gray(` Logs: videan logs --app ${appId}`));
|
|
1131
|
+
process.exit(1);
|
|
1132
|
+
} else {
|
|
1133
|
+
spinner.info(`Deployment status: ${formatStatus(current.status)}`);
|
|
1134
|
+
process.exit(1);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// src/commands/logs.ts
|
|
1139
|
+
import { Command as Command5 } from "commander";
|
|
1140
|
+
import chalk6 from "chalk";
|
|
1141
|
+
var logsCommand = new Command5("logs").description("View app logs").option("-a, --app <id>", "App ID").option("-f, --follow", "Follow log output").option("-n, --lines <n>", "Number of lines to show", "50").option("-s, --since <when>", "Start of the window: ISO timestamp or an offset like 15m, 2h, 7d", "1h").option("--until <when>", "End of the window (same formats as --since)").option("-q, --search <text>", "Only lines containing this text").option("--pattern <pattern>", "CloudWatch filter pattern, for the real syntax").option("--instance <prefix>", "Only one instance, by log stream prefix").action(async (opts) => {
|
|
1142
|
+
const apiKey = getApiKey();
|
|
1143
|
+
requireApiKey(apiKey);
|
|
1144
|
+
const appId = opts.app || getCurrentApp();
|
|
1145
|
+
if (!appId) {
|
|
1146
|
+
console.error(chalk6.red("No app specified. Use --app or link an app."));
|
|
1147
|
+
process.exit(1);
|
|
1148
|
+
}
|
|
1149
|
+
if (opts.search && opts.pattern) {
|
|
1150
|
+
console.error(chalk6.red("Use either --search or --pattern, not both."));
|
|
1151
|
+
process.exit(1);
|
|
1152
|
+
}
|
|
1153
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1154
|
+
const filters = {
|
|
1155
|
+
q: opts.search,
|
|
1156
|
+
pattern: opts.pattern,
|
|
1157
|
+
instance: opts.instance
|
|
1158
|
+
};
|
|
1159
|
+
if (opts.follow) {
|
|
1160
|
+
for await (const entry of client4.apps.followLogs(appId, { since: opts.since, ...filters })) {
|
|
1161
|
+
print(entry);
|
|
1162
|
+
}
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const page = await client4.apps.logs(appId, {
|
|
1166
|
+
limit: parseInt(opts.lines, 10),
|
|
1167
|
+
since: opts.since,
|
|
1168
|
+
until: opts.until,
|
|
1169
|
+
...filters
|
|
1170
|
+
});
|
|
1171
|
+
for (const entry of page.events) print(entry);
|
|
1172
|
+
if (page.events.length === 0) {
|
|
1173
|
+
console.log(chalk6.gray(`No log lines since ${page.start}.`));
|
|
1174
|
+
}
|
|
1175
|
+
if (page.truncated) {
|
|
1176
|
+
console.log(
|
|
1177
|
+
chalk6.yellow(
|
|
1178
|
+
"\nThis is part of the window -- there are more lines than one request reads. Narrow --since or add --search."
|
|
1179
|
+
)
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
function print(entry) {
|
|
1184
|
+
console.log(`${chalk6.gray(entry.timestamp)} ${formatLevel(entry.level)} ${entry.message}`);
|
|
1185
|
+
}
|
|
1186
|
+
function formatLevel(level) {
|
|
1187
|
+
switch (level) {
|
|
1188
|
+
case "error":
|
|
1189
|
+
return chalk6.red("[ERR]");
|
|
1190
|
+
case "warn":
|
|
1191
|
+
return chalk6.yellow("[WRN]");
|
|
1192
|
+
case "info":
|
|
1193
|
+
return chalk6.blue("[INF]");
|
|
1194
|
+
case "debug":
|
|
1195
|
+
return chalk6.gray("[DBG]");
|
|
1196
|
+
default:
|
|
1197
|
+
return chalk6.gray("[LOG]");
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/commands/metrics.ts
|
|
1202
|
+
import { Command as Command6 } from "commander";
|
|
1203
|
+
import chalk7 from "chalk";
|
|
1204
|
+
var metricsCommand = new Command6("metrics").description("Traffic, latency and resource use for an app").option("-a, --app <id>", "App ID").option("-s, --since <when>", "Start of the window: ISO timestamp or an offset like 15m, 2h, 7d", "1h").option("--until <when>", "End of the window (same formats as --since)").option("--series", "Print each bucket instead of just the summary").option("--json", "Print the raw response").action(async (opts) => {
|
|
1205
|
+
const apiKey = getApiKey();
|
|
1206
|
+
requireApiKey(apiKey);
|
|
1207
|
+
const appId = opts.app || getCurrentApp();
|
|
1208
|
+
if (!appId) {
|
|
1209
|
+
console.error(chalk7.red("No app specified. Use --app or link an app."));
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1213
|
+
const metrics = await client4.apps.metrics(appId, { since: opts.since, until: opts.until });
|
|
1214
|
+
if (opts.json) {
|
|
1215
|
+
console.log(JSON.stringify(metrics, null, 2));
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const t = metrics.totals;
|
|
1219
|
+
console.log(
|
|
1220
|
+
chalk7.dim(`${metrics.start} \u2192 ${metrics.end} (${metrics.period}s buckets)`)
|
|
1221
|
+
);
|
|
1222
|
+
console.log();
|
|
1223
|
+
console.log(` ${label("Requests")} ${t.requests}`);
|
|
1224
|
+
console.log(` ${label("4xx")} ${count(t.clientErrors)}`);
|
|
1225
|
+
console.log(` ${label("5xx")} ${count(t.serverErrors, true)}`);
|
|
1226
|
+
console.log(` ${label("Gate denied")} ${count(t.gateDenied)}`);
|
|
1227
|
+
console.log(` ${label("Latency avg")} ${ms(t.latencyAvgMs)}`);
|
|
1228
|
+
console.log(` ${label("Latency p95")} ${ms(t.latencyP95Ms)} ${chalk7.dim("(sampled)")}`);
|
|
1229
|
+
console.log(` ${label("CPU avg")} ${units(t.cpuAvg)}`);
|
|
1230
|
+
console.log(` ${label("Memory avg")} ${mb(t.memoryAvgMb)}`);
|
|
1231
|
+
if (t.requests === 0) {
|
|
1232
|
+
console.log();
|
|
1233
|
+
console.log(chalk7.dim("No requests recorded in this window."));
|
|
1234
|
+
}
|
|
1235
|
+
if (opts.series) {
|
|
1236
|
+
console.log();
|
|
1237
|
+
for (const [name, points] of Object.entries(metrics.series)) {
|
|
1238
|
+
if (points.length === 0) continue;
|
|
1239
|
+
console.log(chalk7.bold(name));
|
|
1240
|
+
for (const p of points) console.log(` ${chalk7.dim(p.t)} ${p.v}`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
function label(text) {
|
|
1245
|
+
return chalk7.dim(text.padEnd(13));
|
|
1246
|
+
}
|
|
1247
|
+
function count(n, severe = false) {
|
|
1248
|
+
if (n === 0) return chalk7.dim("0");
|
|
1249
|
+
return severe ? chalk7.red(String(n)) : chalk7.yellow(String(n));
|
|
1250
|
+
}
|
|
1251
|
+
function ms(v) {
|
|
1252
|
+
return v === null ? chalk7.dim("\u2014") : `${v} ms`;
|
|
1253
|
+
}
|
|
1254
|
+
function mb(v) {
|
|
1255
|
+
return v === null ? chalk7.dim("\u2014") : `${v} MB`;
|
|
1256
|
+
}
|
|
1257
|
+
function units(v) {
|
|
1258
|
+
return v === null ? chalk7.dim("\u2014") : `${v} CPU units`;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// src/commands/rollback.ts
|
|
1262
|
+
import { Command as Command7 } from "commander";
|
|
1263
|
+
import chalk8 from "chalk";
|
|
1264
|
+
import ora3 from "ora";
|
|
1265
|
+
var rollbackCommand = new Command7("rollback").description("Rollback to the previous production deployment").option("-a, --app <id>", "App ID").action(async (opts) => {
|
|
1266
|
+
const apiKey = getApiKey();
|
|
1267
|
+
requireApiKey(apiKey);
|
|
1268
|
+
const appId = opts.app || getCurrentApp();
|
|
1269
|
+
if (!appId) {
|
|
1270
|
+
console.error(chalk8.red("No app specified. Use --app or link an app."));
|
|
1271
|
+
process.exit(1);
|
|
1272
|
+
}
|
|
1273
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1274
|
+
const spinner = ora3("Rolling back...").start();
|
|
1275
|
+
try {
|
|
1276
|
+
const deployment = await client4.apps.rollback(appId);
|
|
1277
|
+
spinner.succeed(chalk8.green("Rollback successful!"));
|
|
1278
|
+
if (deployment.url) {
|
|
1279
|
+
console.log(chalk8.cyan(` URL: ${deployment.url}`));
|
|
1280
|
+
}
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
spinner.fail(chalk8.red(`Rollback failed: ${error.message}`));
|
|
1283
|
+
process.exit(1);
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
// src/commands/secrets.ts
|
|
1288
|
+
import { Command as Command8 } from "commander";
|
|
1289
|
+
import chalk9 from "chalk";
|
|
1290
|
+
import inquirer3 from "inquirer";
|
|
1291
|
+
var secretsCommand = new Command8("secrets").description("Manage app secrets");
|
|
1292
|
+
secretsCommand.command("set <key> [value]").description("Set a secret").option("-a, --app <id>", "App ID").action(async (key, value, opts) => {
|
|
1293
|
+
const apiKey = getApiKey();
|
|
1294
|
+
requireApiKey(apiKey);
|
|
1295
|
+
const appId = opts.app || getCurrentApp();
|
|
1296
|
+
if (!appId) {
|
|
1297
|
+
console.error(chalk9.red("No app specified."));
|
|
1298
|
+
process.exit(1);
|
|
1299
|
+
}
|
|
1300
|
+
if (!value) {
|
|
1301
|
+
const answers = await inquirer3.prompt([
|
|
1302
|
+
{ type: "password", name: "value", message: `Value for ${key}:` }
|
|
1303
|
+
]);
|
|
1304
|
+
value = answers.value;
|
|
1305
|
+
}
|
|
1306
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1307
|
+
await client4.secrets.set(appId, key, value);
|
|
1308
|
+
console.log(chalk9.green(`Secret "${key}" set successfully.`));
|
|
1309
|
+
});
|
|
1310
|
+
secretsCommand.command("list").description("List all secrets").option("-a, --app <id>", "App ID").action(async (opts) => {
|
|
1311
|
+
const apiKey = getApiKey();
|
|
1312
|
+
requireApiKey(apiKey);
|
|
1313
|
+
const appId = opts.app || getCurrentApp();
|
|
1314
|
+
if (!appId) {
|
|
1315
|
+
console.error(chalk9.red("No app specified."));
|
|
1316
|
+
process.exit(1);
|
|
1317
|
+
}
|
|
1318
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1319
|
+
const secrets = await client4.secrets.list(appId);
|
|
1320
|
+
if (secrets.length === 0) {
|
|
1321
|
+
console.log(chalk9.gray("No secrets configured."));
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
console.log(formatRow(["KEY", "UPDATED"], [30, 24]));
|
|
1325
|
+
for (const secret of secrets) {
|
|
1326
|
+
console.log(formatRow([secret.key, secret.updatedAt], [30, 24]));
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
secretsCommand.command("remove <key>").description("Remove a secret").option("-a, --app <id>", "App ID").action(async (key, opts) => {
|
|
1330
|
+
const apiKey = getApiKey();
|
|
1331
|
+
requireApiKey(apiKey);
|
|
1332
|
+
const appId = opts.app || getCurrentApp();
|
|
1333
|
+
if (!appId) {
|
|
1334
|
+
console.error(chalk9.red("No app specified."));
|
|
1335
|
+
process.exit(1);
|
|
1336
|
+
}
|
|
1337
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1338
|
+
await client4.secrets.delete(appId, key);
|
|
1339
|
+
console.log(chalk9.green(`Secret "${key}" removed.`));
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
// src/commands/apps.ts
|
|
1343
|
+
import { Command as Command9 } from "commander";
|
|
1344
|
+
import chalk10 from "chalk";
|
|
1345
|
+
import inquirer4 from "inquirer";
|
|
1346
|
+
var appsCommand = new Command9("apps").description("Manage apps");
|
|
1347
|
+
function accessOption(value) {
|
|
1348
|
+
if (value === void 0) return void 0;
|
|
1349
|
+
const tier = value.trim().toLowerCase();
|
|
1350
|
+
if (!isVisibility(tier)) {
|
|
1351
|
+
console.error(chalk10.red(`Unknown --access value "${value}". Use one of: ${VISIBILITIES.join(", ")}`));
|
|
1352
|
+
process.exit(1);
|
|
1353
|
+
}
|
|
1354
|
+
return tier;
|
|
1355
|
+
}
|
|
1356
|
+
function describeAccess(app) {
|
|
1357
|
+
const tier = app.visibility ? visibilityOf(app.visibility) : app.authRequired ? "org" : "public";
|
|
1358
|
+
return `${tier} \u2014 ${VISIBILITY_LABELS[tier].toLowerCase()}`;
|
|
1359
|
+
}
|
|
1360
|
+
appsCommand.command("list").description("List all apps").option("-o, --org <id>", "Organization ID").action(async (opts) => {
|
|
1361
|
+
const apiKey = getApiKey();
|
|
1362
|
+
requireApiKey(apiKey);
|
|
1363
|
+
const orgId = requireOrg(opts.org || getDefaultOrg());
|
|
1364
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1365
|
+
const apps = await client4.apps.list(orgId);
|
|
1366
|
+
if (apps.length === 0) {
|
|
1367
|
+
console.log(chalk10.gray("No apps yet. Create one with `videan apps create`."));
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const widths = [20, 24, 10, 44];
|
|
1371
|
+
console.log(formatRow(["NAME", "ID", "STATUS", "URL"], widths));
|
|
1372
|
+
for (const app of apps) {
|
|
1373
|
+
console.log(formatRow([app.name, app.id, app.status, appUrl(app.slug)], widths));
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
appsCommand.command("create").description("Create a new app").option("-n, --name <name>", "App name").option("-o, --org <id>", "Organization ID").option("--postgres", "Provision a managed Postgres database").option("--storage", "Give the app an object-storage prefix").option("--email", "Let the app send mail through the platform").option("--public", "Serve the app without the platform login gate (same as --access public)").option("--access <tier>", `Who may open the app: ${VISIBILITIES.join(" | ")} (default: ${DEFAULT_VISIBILITY})`).action(
|
|
1377
|
+
async (opts) => {
|
|
1378
|
+
const apiKey = getApiKey();
|
|
1379
|
+
requireApiKey(apiKey);
|
|
1380
|
+
const orgId = requireOrg(opts.org || getDefaultOrg());
|
|
1381
|
+
let name = opts.name;
|
|
1382
|
+
if (!name) {
|
|
1383
|
+
const answers = await inquirer4.prompt([
|
|
1384
|
+
{ type: "input", name: "name", message: "App name:" }
|
|
1385
|
+
]);
|
|
1386
|
+
name = answers.name;
|
|
1387
|
+
}
|
|
1388
|
+
if (!name) {
|
|
1389
|
+
console.error(chalk10.red("An app name is required."));
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
}
|
|
1392
|
+
const visibility = accessOption(opts.access) ?? (opts.public ? "public" : void 0);
|
|
1393
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1394
|
+
const app = await client4.apps.create({
|
|
1395
|
+
name,
|
|
1396
|
+
orgId,
|
|
1397
|
+
postgres: opts.postgres ?? false,
|
|
1398
|
+
storage: opts.storage ?? false,
|
|
1399
|
+
email: opts.email ?? false,
|
|
1400
|
+
visibility
|
|
1401
|
+
});
|
|
1402
|
+
setCurrentApp(app.id);
|
|
1403
|
+
console.log(chalk10.green(`App "${app.name}" created (${app.id})`));
|
|
1404
|
+
console.log(chalk10.gray(` Access: ${describeAccess(app)}`));
|
|
1405
|
+
console.log(chalk10.gray(` URL after first deploy: ${appUrl(app.slug)}`));
|
|
1406
|
+
console.log(chalk10.gray(" Push code with `videan deploy` \u2014 the runtime is auto-detected."));
|
|
1407
|
+
}
|
|
1408
|
+
);
|
|
1409
|
+
appsCommand.command("set").description("Change an app's settings (scale, rate limit, gate, capabilities)").option("-a, --app <id>", "App ID").option("-n, --name <name>", "Rename the app (the URL keeps its original slug)").option("--cpu <units>", "CPU units per instance (256 = 0.25 vCPU)").option("--memory <mib>", "Memory per instance, in MiB").option("--instances <n>", "Number of instances to run").option("--rate-limit <rpm>", "Requests per minute per client").option("--no-rate-limit", "Remove the rate limit").option("--access <tier>", `Who may open the app: ${VISIBILITIES.join(" | ")}`).option("--public", "Serve without the platform login gate (same as --access public)").option("--private", "Require platform login (leaves the tier alone if it is already gated)").option("--postgres", "Provision a managed Postgres database").option("--storage", "Give the app an object-storage prefix").option("--email", "Let the app send mail through the platform").option("--json", "Print the raw response").action(
|
|
1410
|
+
async (opts) => {
|
|
1411
|
+
const apiKey = getApiKey();
|
|
1412
|
+
requireApiKey(apiKey);
|
|
1413
|
+
const appId = opts.app || getCurrentApp();
|
|
1414
|
+
if (!appId) {
|
|
1415
|
+
console.error(chalk10.red("No app specified. Use --app or link an app."));
|
|
1416
|
+
process.exit(1);
|
|
1417
|
+
}
|
|
1418
|
+
if (opts.public && opts.private) {
|
|
1419
|
+
console.error(chalk10.red("--public and --private contradict each other."));
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
}
|
|
1422
|
+
const access = accessOption(opts.access);
|
|
1423
|
+
if (access && (opts.public || opts.private)) {
|
|
1424
|
+
console.error(chalk10.red("--access already says who may open the app; drop --public/--private."));
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
const changes = {};
|
|
1428
|
+
if (opts.name) changes.name = opts.name;
|
|
1429
|
+
if (opts.cpu !== void 0) changes.cpu = intOption("--cpu", opts.cpu);
|
|
1430
|
+
if (opts.memory !== void 0) changes.memory = intOption("--memory", opts.memory);
|
|
1431
|
+
if (opts.instances !== void 0) {
|
|
1432
|
+
changes.instances = intOption("--instances", opts.instances);
|
|
1433
|
+
}
|
|
1434
|
+
if (opts.rateLimit === false) changes.rateLimitPerMinute = null;
|
|
1435
|
+
else if (typeof opts.rateLimit === "string") {
|
|
1436
|
+
changes.rateLimitPerMinute = intOption("--rate-limit", opts.rateLimit);
|
|
1437
|
+
}
|
|
1438
|
+
if (access) changes.visibility = access;
|
|
1439
|
+
if (opts.public) changes.authRequired = false;
|
|
1440
|
+
if (opts.private) changes.authRequired = true;
|
|
1441
|
+
if (opts.postgres) changes.postgres = true;
|
|
1442
|
+
if (opts.storage) changes.storage = true;
|
|
1443
|
+
if (opts.email) changes.email = true;
|
|
1444
|
+
if (Object.keys(changes).length === 0) {
|
|
1445
|
+
console.error(chalk10.red("Nothing to change. Pass at least one option."));
|
|
1446
|
+
process.exit(1);
|
|
1447
|
+
}
|
|
1448
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1449
|
+
const app = await client4.apps.update(appId, changes);
|
|
1450
|
+
if (opts.json) {
|
|
1451
|
+
console.log(JSON.stringify(app, null, 2));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
console.log(chalk10.green(`Updated ${app.name} (${app.id})`));
|
|
1455
|
+
console.log(` Access: ${describeAccess(app)}`);
|
|
1456
|
+
console.log(` Scale: ${app.cpu ?? "default"} CPU / ${app.memory ?? "default"} MiB`);
|
|
1457
|
+
console.log(
|
|
1458
|
+
` Rate limit: ${app.rateLimitPerMinute ? `${app.rateLimitPerMinute}/min per client` : "none"}`
|
|
1459
|
+
);
|
|
1460
|
+
if (app.restartRequired) {
|
|
1461
|
+
console.log(
|
|
1462
|
+
chalk10.yellow("\n These settings apply to the next deploy. Run `videan deploy` to pick them up.")
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
);
|
|
1467
|
+
function intOption(flag, raw) {
|
|
1468
|
+
const n = Number(raw);
|
|
1469
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
1470
|
+
console.error(chalk10.red(`${flag} expects a positive whole number, got "${raw}".`));
|
|
1471
|
+
process.exit(1);
|
|
1472
|
+
}
|
|
1473
|
+
return n;
|
|
1474
|
+
}
|
|
1475
|
+
appsCommand.command("delete <id>").description("Delete an app and all its resources").action(async (id) => {
|
|
1476
|
+
const apiKey = getApiKey();
|
|
1477
|
+
requireApiKey(apiKey);
|
|
1478
|
+
const { confirm } = await inquirer4.prompt([
|
|
1479
|
+
{
|
|
1480
|
+
type: "confirm",
|
|
1481
|
+
name: "confirm",
|
|
1482
|
+
message: `Delete app ${id}? This stops the app and destroys its database, secrets and snapshots.`,
|
|
1483
|
+
default: false
|
|
1484
|
+
}
|
|
1485
|
+
]);
|
|
1486
|
+
if (!confirm) return;
|
|
1487
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1488
|
+
const { warnings } = await client4.apps.delete(id);
|
|
1489
|
+
console.log(chalk10.green("App deleted."));
|
|
1490
|
+
if (warnings.length > 0) {
|
|
1491
|
+
console.log(chalk10.yellow("\nSome resources could not be released:"));
|
|
1492
|
+
for (const w of warnings) {
|
|
1493
|
+
console.log(chalk10.yellow(` ${w.step}: ${w.error || "unknown error"}`));
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
// src/commands/status.ts
|
|
1499
|
+
import { Command as Command10 } from "commander";
|
|
1500
|
+
import chalk11 from "chalk";
|
|
1501
|
+
function accessLine(app) {
|
|
1502
|
+
const tier = app.visibility ? visibilityOf(app.visibility) : app.authRequired ? "org" : "public";
|
|
1503
|
+
return `${tier} \u2014 ${VISIBILITY_LABELS[tier].toLowerCase()}`;
|
|
1504
|
+
}
|
|
1505
|
+
var statusCommand = new Command10("status").description("Show current app status").option("-a, --app <id>", "App ID").action(async (opts) => {
|
|
1506
|
+
const apiKey = getApiKey();
|
|
1507
|
+
requireApiKey(apiKey);
|
|
1508
|
+
const appId = opts.app || getCurrentApp();
|
|
1509
|
+
if (!appId) {
|
|
1510
|
+
console.error(chalk11.red("No app specified. Use --app or link an app."));
|
|
1511
|
+
process.exit(1);
|
|
1512
|
+
}
|
|
1513
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1514
|
+
const app = await client4.apps.get(appId);
|
|
1515
|
+
const health = await client4.apps.health(appId);
|
|
1516
|
+
console.log(chalk11.bold(app.name));
|
|
1517
|
+
console.log(` ID: ${app.id}`);
|
|
1518
|
+
console.log(` Created: ${app.createdAt}`);
|
|
1519
|
+
console.log(` Postgres: ${app.postgresEnabled ? "enabled" : "disabled"}`);
|
|
1520
|
+
console.log(` Access: ${accessLine(app)}`);
|
|
1521
|
+
console.log(` State: ${formatStatus(health.state)}`);
|
|
1522
|
+
console.log(` Healthy: ${health.healthy ? chalk11.green("yes") : chalk11.red("no")}`);
|
|
1523
|
+
console.log(` URL: ${chalk11.cyan(health.url || appUrl(app.slug))}`);
|
|
1524
|
+
const task = health.task;
|
|
1525
|
+
if (task) {
|
|
1526
|
+
if (task.exit_code !== void 0 && task.exit_code !== null) {
|
|
1527
|
+
const code = String(task.exit_code);
|
|
1528
|
+
console.log(` Exit code: ${task.exit_code === 0 ? code : chalk11.red(code)}`);
|
|
1529
|
+
}
|
|
1530
|
+
if (task.stopped_reason) {
|
|
1531
|
+
console.log(` Stopped: ${chalk11.yellow(task.stopped_reason)}`);
|
|
1532
|
+
}
|
|
1533
|
+
if (task.container_reason) {
|
|
1534
|
+
console.log(` Detail: ${chalk11.yellow(task.container_reason)}`);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
if (!health.healthy) {
|
|
1538
|
+
console.log(chalk11.gray("\nRun `videan logs` to see why."));
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
// src/commands/keys.ts
|
|
1543
|
+
import { Command as Command11 } from "commander";
|
|
1544
|
+
import chalk12 from "chalk";
|
|
1545
|
+
import inquirer5 from "inquirer";
|
|
1546
|
+
var keysCommand = new Command11("keys").description("Manage API keys for programmatic access");
|
|
1547
|
+
keysCommand.command("create <name>").description("Create an API key").option("-o, --org <id>", "Organization ID").option("-e, --expires <days>", "Expire the key after N days").option(
|
|
1548
|
+
"--app <id>",
|
|
1549
|
+
"Pin the key to one app (a service token, for code running inside it)"
|
|
1550
|
+
).action(async (name, opts) => {
|
|
1551
|
+
const apiKey = getApiKey();
|
|
1552
|
+
requireApiKey(apiKey);
|
|
1553
|
+
const orgId = requireOrg(opts.org || getDefaultOrg());
|
|
1554
|
+
let expiresInDays;
|
|
1555
|
+
if (opts.expires !== void 0) {
|
|
1556
|
+
expiresInDays = Number(opts.expires);
|
|
1557
|
+
if (!Number.isInteger(expiresInDays) || expiresInDays < 1) {
|
|
1558
|
+
console.error(chalk12.red("--expires must be a whole number of days, at least 1."));
|
|
1559
|
+
process.exit(1);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1563
|
+
const key = await client4.apiKeys.create({
|
|
1564
|
+
name,
|
|
1565
|
+
orgId,
|
|
1566
|
+
expiresInDays,
|
|
1567
|
+
appId: opts.app
|
|
1568
|
+
});
|
|
1569
|
+
console.log(chalk12.green(`Created key "${key.name}" (${key.id})`));
|
|
1570
|
+
console.log(`
|
|
1571
|
+
${chalk12.bold(key.key)}
|
|
1572
|
+
`);
|
|
1573
|
+
console.log(chalk12.yellow("Copy it now. It cannot be shown again."));
|
|
1574
|
+
if (key.appId) {
|
|
1575
|
+
console.log(
|
|
1576
|
+
chalk12.gray(`Service token, scoped to app ${key.appId}. It cannot reach anything else.`)
|
|
1577
|
+
);
|
|
1578
|
+
} else {
|
|
1579
|
+
console.log(
|
|
1580
|
+
chalk12.gray("Acts as you across every app in the org -- do not deploy it inside an app.")
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
if (key.expiresAt) {
|
|
1584
|
+
console.log(chalk12.gray(`Expires ${key.expiresAt}`));
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
keysCommand.command("list").description("List API keys").option("-o, --org <id>", "Organization ID").action(async (opts) => {
|
|
1588
|
+
const apiKey = getApiKey();
|
|
1589
|
+
requireApiKey(apiKey);
|
|
1590
|
+
const orgId = requireOrg(opts.org || getDefaultOrg());
|
|
1591
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1592
|
+
const keys = await client4.apiKeys.list(orgId);
|
|
1593
|
+
if (keys.length === 0) {
|
|
1594
|
+
console.log(chalk12.gray("No API keys. Create one with `videan keys create <name>`."));
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const widths = [24, 24, 16, 22, 12, 24];
|
|
1598
|
+
console.log(formatRow(["NAME", "ID", "PREFIX", "LAST USED", "STATE", "SCOPE"], widths));
|
|
1599
|
+
for (const key of keys) {
|
|
1600
|
+
const state = key.revokedAt ? chalk12.red("revoked") : key.expiresAt && new Date(key.expiresAt) < /* @__PURE__ */ new Date() ? chalk12.yellow("expired") : chalk12.green("active");
|
|
1601
|
+
console.log(formatRow(
|
|
1602
|
+
[
|
|
1603
|
+
key.name,
|
|
1604
|
+
key.id,
|
|
1605
|
+
`${key.keyPrefix}\u2026`,
|
|
1606
|
+
key.lastUsedAt || "never",
|
|
1607
|
+
state,
|
|
1608
|
+
key.appId ? `app ${key.appId}` : "org-wide"
|
|
1609
|
+
],
|
|
1610
|
+
widths
|
|
1611
|
+
));
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
keysCommand.command("revoke <id>").description("Revoke an API key").action(async (id) => {
|
|
1615
|
+
const apiKey = getApiKey();
|
|
1616
|
+
requireApiKey(apiKey);
|
|
1617
|
+
const { confirm } = await inquirer5.prompt([
|
|
1618
|
+
{ type: "confirm", name: "confirm", message: `Revoke key ${id}?`, default: false }
|
|
1619
|
+
]);
|
|
1620
|
+
if (!confirm) return;
|
|
1621
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1622
|
+
await client4.apiKeys.revoke(id);
|
|
1623
|
+
console.log(chalk12.green("Key revoked."));
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// src/commands/orgs.ts
|
|
1627
|
+
import { Command as Command12 } from "commander";
|
|
1628
|
+
import chalk13 from "chalk";
|
|
1629
|
+
var orgsCommand = new Command12("orgs").description("Manage organizations");
|
|
1630
|
+
orgsCommand.command("list").description("List your organizations").action(async () => {
|
|
1631
|
+
const apiKey = getApiKey();
|
|
1632
|
+
requireApiKey(apiKey);
|
|
1633
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1634
|
+
const orgs = await client4.orgs.list();
|
|
1635
|
+
const current = getDefaultOrg();
|
|
1636
|
+
if (orgs.length === 0) {
|
|
1637
|
+
console.log(chalk13.gray("No organizations. Create one with `videan orgs create <name>`."));
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
const widths = [24, 24, 10, 8];
|
|
1641
|
+
console.log(formatRow(["NAME", "ID", "PLAN", ""], widths));
|
|
1642
|
+
for (const org of orgs) {
|
|
1643
|
+
console.log(formatRow(
|
|
1644
|
+
[org.name, org.id, org.plan, org.id === current ? chalk13.green("default") : ""],
|
|
1645
|
+
widths
|
|
1646
|
+
));
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
orgsCommand.command("create <name>").description("Create an organization").action(async (name) => {
|
|
1650
|
+
const apiKey = getApiKey();
|
|
1651
|
+
requireApiKey(apiKey);
|
|
1652
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1653
|
+
const org = await client4.orgs.create(name);
|
|
1654
|
+
if (!getDefaultOrg()) setDefaultOrg(org.id);
|
|
1655
|
+
console.log(chalk13.green(`Organization "${org.name}" created (${org.id})`));
|
|
1656
|
+
});
|
|
1657
|
+
orgsCommand.command("use <id>").description("Set the default organization for this machine").action(async (id) => {
|
|
1658
|
+
const apiKey = getApiKey();
|
|
1659
|
+
requireApiKey(apiKey);
|
|
1660
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1661
|
+
const org = await client4.orgs.get(id);
|
|
1662
|
+
setDefaultOrg(org.id);
|
|
1663
|
+
console.log(chalk13.green(`Default organization set to "${org.name}".`));
|
|
1664
|
+
});
|
|
1665
|
+
orgsCommand.command("members").description("List organization members").option("-o, --org <id>", "Organization ID").action(async (opts) => {
|
|
1666
|
+
const apiKey = getApiKey();
|
|
1667
|
+
requireApiKey(apiKey);
|
|
1668
|
+
const orgId = requireOrg(opts.org || getDefaultOrg());
|
|
1669
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1670
|
+
const members = await client4.orgs.members(orgId);
|
|
1671
|
+
const widths = [32, 24, 10];
|
|
1672
|
+
console.log(formatRow(["EMAIL", "NAME", "ROLE"], widths));
|
|
1673
|
+
for (const member of members) {
|
|
1674
|
+
console.log(formatRow(
|
|
1675
|
+
[member.email || member.user_id, member.name || "-", member.role],
|
|
1676
|
+
widths
|
|
1677
|
+
));
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
orgsCommand.command("invite <email>").description("Invite a member by email").option("-o, --org <id>", "Organization ID").action(async (email, opts) => {
|
|
1681
|
+
const apiKey = getApiKey();
|
|
1682
|
+
requireApiKey(apiKey);
|
|
1683
|
+
const orgId = requireOrg(opts.org || getDefaultOrg());
|
|
1684
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1685
|
+
await client4.orgs.invite(orgId, email);
|
|
1686
|
+
console.log(chalk13.green(`Invited ${email}.`));
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
// src/commands/storage.ts
|
|
1690
|
+
import { Command as Command13 } from "commander";
|
|
1691
|
+
import chalk14 from "chalk";
|
|
1692
|
+
import fs3 from "fs-extra";
|
|
1693
|
+
import path3 from "node:path";
|
|
1694
|
+
var storageCommand = new Command13("storage").description("Store files for an app");
|
|
1695
|
+
function client() {
|
|
1696
|
+
const apiKey = getApiKey();
|
|
1697
|
+
requireApiKey(apiKey);
|
|
1698
|
+
return new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1699
|
+
}
|
|
1700
|
+
function requireApp(opt) {
|
|
1701
|
+
const appId = opt || getCurrentApp();
|
|
1702
|
+
if (!appId) {
|
|
1703
|
+
console.error(chalk14.red("No app specified. Use --app or link an app."));
|
|
1704
|
+
process.exit(1);
|
|
1705
|
+
}
|
|
1706
|
+
return appId;
|
|
1707
|
+
}
|
|
1708
|
+
function human(bytes) {
|
|
1709
|
+
const units2 = ["B", "KB", "MB", "GB", "TB"];
|
|
1710
|
+
let n = bytes;
|
|
1711
|
+
let i = 0;
|
|
1712
|
+
while (n >= 1024 && i < units2.length - 1) {
|
|
1713
|
+
n /= 1024;
|
|
1714
|
+
i++;
|
|
1715
|
+
}
|
|
1716
|
+
return `${i === 0 ? n : n.toFixed(1)} ${units2[i]}`;
|
|
1717
|
+
}
|
|
1718
|
+
storageCommand.command("ls [prefix]").description("List stored objects").option("-a, --app <id>", "App ID").option("--json", "Print the raw response").action(async (prefix, opts) => {
|
|
1719
|
+
const appId = requireApp(opts.app);
|
|
1720
|
+
const objects = await client().storage.listAll(appId, prefix);
|
|
1721
|
+
if (opts.json) {
|
|
1722
|
+
console.log(JSON.stringify(objects, null, 2));
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
if (objects.length === 0) {
|
|
1726
|
+
console.log(chalk14.gray("No objects stored. Upload one with `videan storage put <file>`."));
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
const widths = [52, 12, 24];
|
|
1730
|
+
console.log(formatRow(["KEY", "SIZE", "MODIFIED"], widths));
|
|
1731
|
+
for (const o of objects) {
|
|
1732
|
+
console.log(formatRow([o.key, human(o.size), o.lastModified || "-"], widths));
|
|
1733
|
+
}
|
|
1734
|
+
const total = objects.reduce((sum, o) => sum + o.size, 0);
|
|
1735
|
+
console.log(chalk14.dim(`
|
|
1736
|
+
${objects.length} object(s), ${human(total)}`));
|
|
1737
|
+
});
|
|
1738
|
+
storageCommand.command("put <file> [key]").description("Upload a file (key defaults to the file's name)").option("-a, --app <id>", "App ID").option("-t, --type <mime>", "Content type").action(async (file, key, opts) => {
|
|
1739
|
+
const appId = requireApp(opts.app);
|
|
1740
|
+
if (!await fs3.pathExists(file)) {
|
|
1741
|
+
console.error(chalk14.red(`No such file: ${file}`));
|
|
1742
|
+
process.exit(1);
|
|
1743
|
+
}
|
|
1744
|
+
const body = await fs3.readFile(file);
|
|
1745
|
+
const objectKey = key || path3.basename(file);
|
|
1746
|
+
const stored = await client().storage.upload(appId, objectKey, body, {
|
|
1747
|
+
contentType: opts.type
|
|
1748
|
+
});
|
|
1749
|
+
console.log(
|
|
1750
|
+
chalk14.green(`Uploaded ${objectKey}`) + chalk14.dim(` (${human(body.length)})`)
|
|
1751
|
+
);
|
|
1752
|
+
if (stored.etag) console.log(chalk14.dim(`etag ${stored.etag}`));
|
|
1753
|
+
});
|
|
1754
|
+
storageCommand.command("get <key> [file]").description("Download an object (defaults to the key's basename)").option("-a, --app <id>", "App ID").action(async (key, file, opts) => {
|
|
1755
|
+
const appId = requireApp(opts.app);
|
|
1756
|
+
const bytes = await client().storage.download(appId, key);
|
|
1757
|
+
const dest = file || path3.basename(key);
|
|
1758
|
+
await fs3.writeFile(dest, bytes);
|
|
1759
|
+
console.log(chalk14.green(`Wrote ${dest}`) + chalk14.dim(` (${human(bytes.length)})`));
|
|
1760
|
+
});
|
|
1761
|
+
storageCommand.command("rm <key>").description("Delete an object").option("-a, --app <id>", "App ID").action(async (key, opts) => {
|
|
1762
|
+
const appId = requireApp(opts.app);
|
|
1763
|
+
await client().storage.delete(appId, key);
|
|
1764
|
+
console.log(chalk14.green(`Deleted ${key}`));
|
|
1765
|
+
});
|
|
1766
|
+
storageCommand.command("url <key>").description("Print a temporary link to an object").option("-a, --app <id>", "App ID").option("-e, --expires <seconds>", "Lifetime in seconds (30-3600)", "900").option("--upload", "Sign an upload (PUT) URL instead of a download").option("-d, --download <filename>", "Serve as a download with this filename").action(async (key, opts) => {
|
|
1767
|
+
const appId = requireApp(opts.app);
|
|
1768
|
+
const expiresIn = Number(opts.expires);
|
|
1769
|
+
if (!Number.isInteger(expiresIn) || expiresIn < 30 || expiresIn > 3600) {
|
|
1770
|
+
console.error(chalk14.red("--expires must be a whole number of seconds between 30 and 3600."));
|
|
1771
|
+
process.exit(1);
|
|
1772
|
+
}
|
|
1773
|
+
const signed = await client().storage.presign(appId, key, {
|
|
1774
|
+
method: opts.upload ? "put" : "get",
|
|
1775
|
+
expiresIn,
|
|
1776
|
+
download: opts.download
|
|
1777
|
+
});
|
|
1778
|
+
console.log(signed.url);
|
|
1779
|
+
console.error(
|
|
1780
|
+
chalk14.yellow(
|
|
1781
|
+
`This ${signed.method} link works for ${expiresIn}s for anyone who has it.`
|
|
1782
|
+
)
|
|
1783
|
+
);
|
|
1784
|
+
});
|
|
1785
|
+
storageCommand.command("usage").description("Total objects and bytes stored").option("-a, --app <id>", "App ID").action(async (opts) => {
|
|
1786
|
+
const appId = requireApp(opts.app);
|
|
1787
|
+
const { objects, bytes } = await client().storage.usage(appId);
|
|
1788
|
+
console.log(`${objects} object(s), ${human(bytes)}`);
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
// src/commands/email.ts
|
|
1792
|
+
import { Command as Command14 } from "commander";
|
|
1793
|
+
import chalk15 from "chalk";
|
|
1794
|
+
import fs4 from "fs-extra";
|
|
1795
|
+
var emailCommand = new Command14("email").description("Send email from an app");
|
|
1796
|
+
function client2() {
|
|
1797
|
+
const apiKey = getApiKey();
|
|
1798
|
+
requireApiKey(apiKey);
|
|
1799
|
+
return new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1800
|
+
}
|
|
1801
|
+
function requireApp2(opt) {
|
|
1802
|
+
const appId = opt || getCurrentApp();
|
|
1803
|
+
if (!appId) {
|
|
1804
|
+
console.error(chalk15.red("No app specified. Use --app or link an app."));
|
|
1805
|
+
process.exit(1);
|
|
1806
|
+
}
|
|
1807
|
+
return appId;
|
|
1808
|
+
}
|
|
1809
|
+
emailCommand.command("send").description("Send an email").requiredOption("-t, --to <address...>", "Recipient address (repeatable)").requiredOption("-s, --subject <subject>", "Subject line").option("--text <body>", "Plain-text body").option("--html <body>", "HTML body").option("--body-file <path>", "Read the body from a file").option("--reply-to <address>", "Where replies should go").option("-a, --app <id>", "App ID").option("--json", "Print the raw response").action(
|
|
1810
|
+
async (opts) => {
|
|
1811
|
+
const appId = requireApp2(opts.app);
|
|
1812
|
+
let text = opts.text;
|
|
1813
|
+
let html = opts.html;
|
|
1814
|
+
if (opts.bodyFile) {
|
|
1815
|
+
const contents = await fs4.readFile(opts.bodyFile, "utf-8");
|
|
1816
|
+
if (/\.html?$/i.test(opts.bodyFile)) html = contents;
|
|
1817
|
+
else text = contents;
|
|
1818
|
+
}
|
|
1819
|
+
if (!text && !html) {
|
|
1820
|
+
console.error(chalk15.red("Nothing to send. Pass --text, --html or --body-file."));
|
|
1821
|
+
process.exit(1);
|
|
1822
|
+
}
|
|
1823
|
+
const sent = await client2().email.send(appId, {
|
|
1824
|
+
to: opts.to,
|
|
1825
|
+
subject: opts.subject,
|
|
1826
|
+
text,
|
|
1827
|
+
html,
|
|
1828
|
+
replyTo: opts.replyTo
|
|
1829
|
+
});
|
|
1830
|
+
if (opts.json) {
|
|
1831
|
+
console.log(JSON.stringify(sent, null, 2));
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
console.log(chalk15.green("Sent"));
|
|
1835
|
+
console.log(` From: ${sent.from}`);
|
|
1836
|
+
console.log(` To: ${sent.to.join(", ")}`);
|
|
1837
|
+
console.log(` Message: ${sent.messageId ?? "(no id returned)"}`);
|
|
1838
|
+
console.log(
|
|
1839
|
+
chalk15.dim(
|
|
1840
|
+
` Quota: ${sent.quota.sent}/${sent.quota.quota} today, resets ${sent.quota.resetsAt}`
|
|
1841
|
+
)
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
);
|
|
1845
|
+
emailCommand.command("quota").description("Show today's sending allowance").option("-a, --app <id>", "App ID").option("--json", "Print the raw response").action(async (opts) => {
|
|
1846
|
+
const quota = await client2().email.quota(requireApp2(opts.app));
|
|
1847
|
+
if (opts.json) {
|
|
1848
|
+
console.log(JSON.stringify(quota, null, 2));
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
console.log(`Sent today: ${quota.sent}`);
|
|
1852
|
+
console.log(`Daily quota: ${quota.quota}`);
|
|
1853
|
+
console.log(`Remaining: ${quota.remaining}`);
|
|
1854
|
+
console.log(chalk15.dim(`Resets at: ${quota.resetsAt}`));
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
// src/commands/jobs.ts
|
|
1858
|
+
import { Command as Command15 } from "commander";
|
|
1859
|
+
import chalk16 from "chalk";
|
|
1860
|
+
import fs5 from "fs-extra";
|
|
1861
|
+
var jobsCommand = new Command15("jobs").description("Enqueue and inspect background jobs");
|
|
1862
|
+
function client3() {
|
|
1863
|
+
const apiKey = getApiKey();
|
|
1864
|
+
requireApiKey(apiKey);
|
|
1865
|
+
return new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1866
|
+
}
|
|
1867
|
+
function requireApp3(opt) {
|
|
1868
|
+
const appId = opt || getCurrentApp();
|
|
1869
|
+
if (!appId) {
|
|
1870
|
+
console.error(chalk16.red("No app specified. Use --app or run `videan init` first."));
|
|
1871
|
+
process.exit(1);
|
|
1872
|
+
}
|
|
1873
|
+
return appId;
|
|
1874
|
+
}
|
|
1875
|
+
function intOption2(flag, raw) {
|
|
1876
|
+
const n = Number(raw);
|
|
1877
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
1878
|
+
console.error(chalk16.red(`${flag} expects a whole number, got "${raw}".`));
|
|
1879
|
+
process.exit(1);
|
|
1880
|
+
}
|
|
1881
|
+
return n;
|
|
1882
|
+
}
|
|
1883
|
+
var STATUS_COLOR = {
|
|
1884
|
+
queued: chalk16.yellow,
|
|
1885
|
+
running: chalk16.cyan,
|
|
1886
|
+
succeeded: chalk16.green,
|
|
1887
|
+
dead: chalk16.red,
|
|
1888
|
+
cancelled: chalk16.gray
|
|
1889
|
+
};
|
|
1890
|
+
function statusLabel(status) {
|
|
1891
|
+
return (STATUS_COLOR[status] ?? chalk16.white)(status);
|
|
1892
|
+
}
|
|
1893
|
+
jobsCommand.command("enqueue").description("Queue a job for the app to run").option("-a, --app <id>", "App ID").option("-q, --queue <name>", "Queue name (default: default)").option("-p, --path <path>", "Path on the app to POST to (default: /jobs)").option("-d, --data <json>", "JSON payload").option("-f, --file <path>", "Read the JSON payload from a file").option("--delay <seconds>", "Run no earlier than this many seconds from now").option("--attempts <n>", "Attempts before the job is given up on (default: 3)").option("--timeout <seconds>", "How long the handler gets per attempt (default: 60)").option("--key <key>", "Idempotency key -- a repeat returns the same job").option("--json", "Print the raw response").action(async (opts) => {
|
|
1894
|
+
const appId = requireApp3(opts.app);
|
|
1895
|
+
let payload;
|
|
1896
|
+
const raw = opts.file ? await fs5.readFile(opts.file, "utf-8") : opts.data;
|
|
1897
|
+
if (raw !== void 0) {
|
|
1898
|
+
try {
|
|
1899
|
+
payload = JSON.parse(raw);
|
|
1900
|
+
} catch (e) {
|
|
1901
|
+
console.error(chalk16.red(`--${opts.file ? "file" : "data"} is not valid JSON: ${e.message}`));
|
|
1902
|
+
process.exit(1);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
const job = await client3().jobs.enqueue(appId, {
|
|
1906
|
+
queue: opts.queue,
|
|
1907
|
+
path: opts.path,
|
|
1908
|
+
payload,
|
|
1909
|
+
delaySeconds: opts.delay !== void 0 ? intOption2("--delay", opts.delay) : void 0,
|
|
1910
|
+
maxAttempts: opts.attempts !== void 0 ? intOption2("--attempts", opts.attempts) : void 0,
|
|
1911
|
+
timeoutSeconds: opts.timeout !== void 0 ? intOption2("--timeout", opts.timeout) : void 0,
|
|
1912
|
+
idempotencyKey: opts.key
|
|
1913
|
+
});
|
|
1914
|
+
if (opts.json) {
|
|
1915
|
+
console.log(JSON.stringify(job, null, 2));
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
if (job.deduplicated) {
|
|
1919
|
+
console.log(chalk16.yellow(`Already queued as ${job.id} (idempotency key "${opts.key}")`));
|
|
1920
|
+
} else {
|
|
1921
|
+
console.log(chalk16.green(`Queued ${job.id}`));
|
|
1922
|
+
}
|
|
1923
|
+
console.log(chalk16.gray(` ${job.method} ${job.path} on queue "${job.queue}"`));
|
|
1924
|
+
if (job.runAt && Date.parse(job.runAt) > Date.now() + 1e3) {
|
|
1925
|
+
console.log(chalk16.gray(` Runs at ${new Date(job.runAt).toLocaleString()}`));
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
jobsCommand.command("list").description("List jobs, newest first").option("-a, --app <id>", "App ID").option("-s, --status <status>", "queued, running, succeeded, dead or cancelled").option("-q, --queue <name>", "Only this queue").option("-n, --limit <n>", "How many to show (default: 20)").option("--json", "Print the raw response").action(async (opts) => {
|
|
1929
|
+
const appId = requireApp3(opts.app);
|
|
1930
|
+
const { jobs, counts } = await client3().jobs.list(appId, {
|
|
1931
|
+
status: opts.status,
|
|
1932
|
+
queue: opts.queue,
|
|
1933
|
+
limit: opts.limit !== void 0 ? intOption2("--limit", opts.limit) : 20
|
|
1934
|
+
});
|
|
1935
|
+
if (opts.json) {
|
|
1936
|
+
console.log(JSON.stringify({ jobs, counts }, null, 2));
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
const depth = Object.entries(counts).map(([status, n]) => `${statusLabel(status)} ${n}`).join(" ");
|
|
1940
|
+
console.log(depth || chalk16.gray("No jobs yet."));
|
|
1941
|
+
if (jobs.length === 0) return;
|
|
1942
|
+
console.log();
|
|
1943
|
+
for (const job of jobs) {
|
|
1944
|
+
const attempts = job.attempts > 0 ? chalk16.gray(` ${job.attempts}/${job.maxAttempts}`) : "";
|
|
1945
|
+
console.log(
|
|
1946
|
+
`${statusLabel(job.status).padEnd(20)} ${chalk16.white(job.id)} ${chalk16.gray(
|
|
1947
|
+
`${job.queue} ${job.path}`
|
|
1948
|
+
)}${attempts}`
|
|
1949
|
+
);
|
|
1950
|
+
if (job.error) console.log(chalk16.red(` ${job.error}`));
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
jobsCommand.command("get <job-id>").description("Show one job in full").option("-a, --app <id>", "App ID").option("--json", "Print the raw response").action(async (jobId, opts) => {
|
|
1954
|
+
const job = await client3().jobs.get(requireApp3(opts.app), jobId);
|
|
1955
|
+
if (opts.json) {
|
|
1956
|
+
console.log(JSON.stringify(job, null, 2));
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
console.log(`${chalk16.white(job.id)} ${statusLabel(job.status)}`);
|
|
1960
|
+
console.log(chalk16.gray(` ${job.method} ${job.path} on queue "${job.queue}"`));
|
|
1961
|
+
console.log(chalk16.gray(` Attempts: ${job.attempts}/${job.maxAttempts}`));
|
|
1962
|
+
if (job.statusCode) console.log(chalk16.gray(` Last response: HTTP ${job.statusCode}`));
|
|
1963
|
+
if (job.durationMs) console.log(chalk16.gray(` Took: ${job.durationMs}ms`));
|
|
1964
|
+
if (job.error) console.log(chalk16.red(` Error: ${job.error}`));
|
|
1965
|
+
if (job.status === "queued" && job.attempts > 0) {
|
|
1966
|
+
console.log(chalk16.yellow(` Retrying at ${new Date(job.runAt).toLocaleString()}`));
|
|
1967
|
+
}
|
|
1968
|
+
if (job.payload !== null && job.payload !== void 0) {
|
|
1969
|
+
console.log(chalk16.gray(" Payload:"));
|
|
1970
|
+
console.log(
|
|
1971
|
+
JSON.stringify(job.payload, null, 2).split("\n").map((l) => ` ${l}`).join("\n")
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
jobsCommand.command("retry <job-id>").description("Run a dead job again, from a clean attempt count").option("-a, --app <id>", "App ID").action(async (jobId, opts) => {
|
|
1976
|
+
const job = await client3().jobs.retry(requireApp3(opts.app), jobId);
|
|
1977
|
+
console.log(chalk16.green(`Requeued ${job.id}`));
|
|
1978
|
+
});
|
|
1979
|
+
jobsCommand.command("cancel <job-id>").description("Cancel a job that has not started yet").option("-a, --app <id>", "App ID").action(async (jobId, opts) => {
|
|
1980
|
+
const job = await client3().jobs.cancel(requireApp3(opts.app), jobId);
|
|
1981
|
+
console.log(chalk16.green(`Cancelled ${job.id}`));
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
// src/commands/usage.ts
|
|
1985
|
+
import { Command as Command16 } from "commander";
|
|
1986
|
+
import chalk17 from "chalk";
|
|
1987
|
+
var usageCommand = new Command16("usage").description("Metered usage and cost for the current month").option("-o, --org <id>", "Organization ID").option("-a, --app <id>", "Show one app's share instead of the whole org").option("-p, --period <YYYY-MM>", "Month to report on (default: the month in progress)").option("--json", "Print the raw response").action(async (opts) => {
|
|
1988
|
+
const apiKey = getApiKey();
|
|
1989
|
+
requireApiKey(apiKey);
|
|
1990
|
+
const client4 = new CloudClient({ apiKey, baseUrl: getBaseUrl() });
|
|
1991
|
+
if (opts.app || !opts.org && getCurrentApp() && !getDefaultOrg()) {
|
|
1992
|
+
const appId = opts.app || getCurrentApp();
|
|
1993
|
+
const report2 = await client4.apps.usage(appId, opts.period);
|
|
1994
|
+
if (opts.json) return void console.log(JSON.stringify(report2, null, 2));
|
|
1995
|
+
console.log(chalk17.dim(`${period(report2.periodStart, report2.periodEnd)} app ${appId}`));
|
|
1996
|
+
console.log();
|
|
1997
|
+
printMetrics(report2.metrics);
|
|
1998
|
+
console.log();
|
|
1999
|
+
for (const line of report2.lines) {
|
|
2000
|
+
console.log(` ${chalk17.dim(line.label.padEnd(22))} ${money(line.cents)}`);
|
|
2001
|
+
}
|
|
2002
|
+
console.log(` ${chalk17.bold("Share of the bill".padEnd(22))} ${chalk17.bold(report2.cost)}`);
|
|
2003
|
+
console.log();
|
|
2004
|
+
console.log(
|
|
2005
|
+
chalk17.dim("At list price, with no plan allowance applied \u2014 see `videan usage` for the org total.")
|
|
2006
|
+
);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
const orgId = opts.org || getDefaultOrg();
|
|
2010
|
+
if (!orgId) {
|
|
2011
|
+
console.error(chalk17.red("No org specified. Use --org, or --app for a single app."));
|
|
2012
|
+
process.exit(1);
|
|
2013
|
+
}
|
|
2014
|
+
const report = await client4.orgs.usage(orgId, opts.period);
|
|
2015
|
+
if (opts.json) return void console.log(JSON.stringify(report, null, 2));
|
|
2016
|
+
const inv = report.invoice;
|
|
2017
|
+
console.log(
|
|
2018
|
+
chalk17.dim(`${period(report.periodStart, report.periodEnd)} org ${orgId} plan ${report.plan}`)
|
|
2019
|
+
);
|
|
2020
|
+
console.log();
|
|
2021
|
+
printMetrics(report.metrics);
|
|
2022
|
+
if (report.apps.length > 0) {
|
|
2023
|
+
console.log();
|
|
2024
|
+
console.log(chalk17.bold("By app"));
|
|
2025
|
+
for (const app of [...report.apps].sort((a, b) => b.cents - a.cents)) {
|
|
2026
|
+
const name = app.slug || app.name || app.appId || "(deleted apps)";
|
|
2027
|
+
console.log(` ${chalk17.dim(String(name).padEnd(24))} ${money(app.cents)}`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
console.log();
|
|
2031
|
+
for (const line of inv.lines) {
|
|
2032
|
+
console.log(
|
|
2033
|
+
` ${chalk17.dim(line.label.padEnd(22))} ${money(line.cents)} ${chalk17.dim(
|
|
2034
|
+
`(${line.quantity} \xD7 ${line.unitPriceCents}\xA2)`
|
|
2035
|
+
)}`
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
console.log(` ${chalk17.dim("Usage".padEnd(22))} ${money(inv.usageCents)}`);
|
|
2039
|
+
if (inv.includedUsageCents > 0) {
|
|
2040
|
+
console.log(
|
|
2041
|
+
` ${chalk17.dim("Included".padEnd(22))} ${chalk17.green(`-${money(Math.min(inv.usageCents, inv.includedUsageCents))}`)}`
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
if (inv.baseCents > 0) {
|
|
2045
|
+
console.log(` ${chalk17.dim("Plan".padEnd(22))} ${money(inv.baseCents)}`);
|
|
2046
|
+
}
|
|
2047
|
+
console.log(` ${chalk17.bold("Total".padEnd(22))} ${chalk17.bold(report.total)}`);
|
|
2048
|
+
console.log();
|
|
2049
|
+
console.log(chalk17.dim("Usage to date. The period is priced as it stands right now."));
|
|
2050
|
+
});
|
|
2051
|
+
usageCommand.command("plans").description("The plan catalogue and what each one allows").option("--json", "Print the raw response").action(async (opts) => {
|
|
2052
|
+
const client4 = new CloudClient({ apiKey: getApiKey() || "", baseUrl: getBaseUrl() });
|
|
2053
|
+
const plans = await client4.plans();
|
|
2054
|
+
if (opts.json) return void console.log(JSON.stringify(plans, null, 2));
|
|
2055
|
+
for (const plan of plans) {
|
|
2056
|
+
console.log(
|
|
2057
|
+
`${chalk17.bold(plan.name)} ${chalk17.dim(`(${plan.id})`)} ${plan.base}/mo` + (plan.includedUsageCents > 0 ? chalk17.dim(`, ${plan.includedUsage} usage included`) : "")
|
|
2058
|
+
);
|
|
2059
|
+
const l = plan.limits;
|
|
2060
|
+
console.log(
|
|
2061
|
+
chalk17.dim(
|
|
2062
|
+
` ${l.apps} apps \xB7 ${l.instancesPerApp} instances/app \xB7 up to ${l.maxCpu} CPU / ${l.maxMemory} MB \xB7 ${l.maxStorageMb} MB storage${l.postgres ? " \xB7 postgres" : ""}${l.customDomains ? " \xB7 custom domains" : ""}`
|
|
2063
|
+
)
|
|
2064
|
+
);
|
|
2065
|
+
console.log();
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
function printMetrics(metrics) {
|
|
2069
|
+
if (metrics.length === 0) {
|
|
2070
|
+
console.log(chalk17.dim(" Nothing metered in this period yet."));
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
for (const m of metrics) {
|
|
2074
|
+
console.log(` ${chalk17.dim(m.metric.padEnd(22))} ${round(m.quantity)}`);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
function round(n) {
|
|
2078
|
+
const v = Math.round(n * 100) / 100;
|
|
2079
|
+
return Number.isInteger(v) ? String(v) : v.toFixed(2);
|
|
2080
|
+
}
|
|
2081
|
+
function money(cents) {
|
|
2082
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
2083
|
+
}
|
|
2084
|
+
function period(start, end) {
|
|
2085
|
+
return `${start.slice(0, 10)} \u2192 ${end.slice(0, 10)}`;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/index.ts
|
|
2089
|
+
var program = new Command17();
|
|
2090
|
+
program.name("videan").description("Videan CLI \u2014 deploy apps in seconds").version("0.1.0");
|
|
2091
|
+
program.addCommand(loginCommand);
|
|
2092
|
+
program.addCommand(logoutCommand);
|
|
2093
|
+
program.addCommand(initCommand);
|
|
2094
|
+
program.addCommand(deployCommand);
|
|
2095
|
+
program.addCommand(logsCommand);
|
|
2096
|
+
program.addCommand(metricsCommand);
|
|
2097
|
+
program.addCommand(rollbackCommand);
|
|
2098
|
+
program.addCommand(secretsCommand);
|
|
2099
|
+
program.addCommand(appsCommand);
|
|
2100
|
+
program.addCommand(statusCommand);
|
|
2101
|
+
program.addCommand(keysCommand);
|
|
2102
|
+
program.addCommand(orgsCommand);
|
|
2103
|
+
program.addCommand(storageCommand);
|
|
2104
|
+
program.addCommand(emailCommand);
|
|
2105
|
+
program.addCommand(jobsCommand);
|
|
2106
|
+
program.addCommand(usageCommand);
|
|
2107
|
+
function die(e) {
|
|
2108
|
+
console.error(`Error: ${e?.message || e}`);
|
|
2109
|
+
if (process.env.VIDEAN_DEBUG && e?.stack) console.error(e.stack);
|
|
2110
|
+
process.exit(1);
|
|
2111
|
+
}
|
|
2112
|
+
process.on("unhandledRejection", die);
|
|
2113
|
+
program.parseAsync().catch(die);
|
|
2114
|
+
//# sourceMappingURL=index.js.map
|