deploro 0.6.2 → 0.8.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/dist/index.js +558 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -230,6 +230,10 @@ Downloading ${keys.length} file(s) to ${opts.storage} \u2026`);
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
// src/commands/gen.ts
|
|
234
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
235
|
+
import { dirname as dirname3 } from "path";
|
|
236
|
+
|
|
233
237
|
// src/lib/api.ts
|
|
234
238
|
function errorMessage(status, body, statusText) {
|
|
235
239
|
const parts = [body.error ?? statusText];
|
|
@@ -268,6 +272,237 @@ async function apiRequestNoProject(path, options) {
|
|
|
268
272
|
return res.json();
|
|
269
273
|
}
|
|
270
274
|
|
|
275
|
+
// src/lib/gen-types.ts
|
|
276
|
+
var STRING_NUMERIC = /* @__PURE__ */ new Set(["int8", "numeric", "money"]);
|
|
277
|
+
var NUMBER = /* @__PURE__ */ new Set(["int2", "int4", "float4", "float8", "oid", "xid", "cid", "regclass", "regproc", "regtype"]);
|
|
278
|
+
var STRING = /* @__PURE__ */ new Set([
|
|
279
|
+
"text",
|
|
280
|
+
"varchar",
|
|
281
|
+
"bpchar",
|
|
282
|
+
"char",
|
|
283
|
+
"name",
|
|
284
|
+
"citext",
|
|
285
|
+
"uuid",
|
|
286
|
+
"inet",
|
|
287
|
+
"cidr",
|
|
288
|
+
"macaddr",
|
|
289
|
+
"macaddr8",
|
|
290
|
+
"date",
|
|
291
|
+
"time",
|
|
292
|
+
"timetz",
|
|
293
|
+
"timestamp",
|
|
294
|
+
"timestamptz",
|
|
295
|
+
"interval",
|
|
296
|
+
"xml",
|
|
297
|
+
"tsvector",
|
|
298
|
+
"tsquery",
|
|
299
|
+
"bit",
|
|
300
|
+
"varbit",
|
|
301
|
+
"point",
|
|
302
|
+
"line",
|
|
303
|
+
"lseg",
|
|
304
|
+
"box",
|
|
305
|
+
"path",
|
|
306
|
+
"polygon",
|
|
307
|
+
"circle",
|
|
308
|
+
"ltree"
|
|
309
|
+
]);
|
|
310
|
+
var DATA_TYPE_TO_UDT = {
|
|
311
|
+
smallint: "int2",
|
|
312
|
+
integer: "int4",
|
|
313
|
+
bigint: "int8",
|
|
314
|
+
real: "float4",
|
|
315
|
+
"double precision": "float8",
|
|
316
|
+
numeric: "numeric",
|
|
317
|
+
money: "money",
|
|
318
|
+
boolean: "bool",
|
|
319
|
+
text: "text",
|
|
320
|
+
"character varying": "varchar",
|
|
321
|
+
character: "bpchar",
|
|
322
|
+
uuid: "uuid",
|
|
323
|
+
json: "json",
|
|
324
|
+
jsonb: "jsonb",
|
|
325
|
+
date: "date",
|
|
326
|
+
bytea: "bytea",
|
|
327
|
+
"timestamp without time zone": "timestamp",
|
|
328
|
+
"timestamp with time zone": "timestamptz",
|
|
329
|
+
"time without time zone": "time",
|
|
330
|
+
"time with time zone": "timetz",
|
|
331
|
+
interval: "interval"
|
|
332
|
+
};
|
|
333
|
+
var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
334
|
+
function key(name) {
|
|
335
|
+
return IDENT_RE.test(name) ? name : JSON.stringify(name);
|
|
336
|
+
}
|
|
337
|
+
function literal(value) {
|
|
338
|
+
return JSON.stringify(value);
|
|
339
|
+
}
|
|
340
|
+
function udtOf(prop) {
|
|
341
|
+
if (prop["x-pg-type"]) return prop["x-pg-type"];
|
|
342
|
+
const dataType = prop.description?.replace(/ NOT NULL$/, "");
|
|
343
|
+
return dataType ? DATA_TYPE_TO_UDT[dataType] ?? null : null;
|
|
344
|
+
}
|
|
345
|
+
function tsType(udt, enums, write, prop) {
|
|
346
|
+
if (udt?.startsWith("_")) {
|
|
347
|
+
const inner = tsType(udt.slice(1), enums, write);
|
|
348
|
+
return /[|\s]/.test(inner) ? `(${inner})[]` : `${inner}[]`;
|
|
349
|
+
}
|
|
350
|
+
if (udt && enums[udt]) return `Database["public"]["Enums"][${literal(udt)}]`;
|
|
351
|
+
if (udt === "bool") return "boolean";
|
|
352
|
+
if (udt === "json" || udt === "jsonb") return "Json";
|
|
353
|
+
if (udt && NUMBER.has(udt)) return "number";
|
|
354
|
+
if (udt && STRING_NUMERIC.has(udt)) return write ? "number | string" : "string";
|
|
355
|
+
if (udt && STRING.has(udt)) return "string";
|
|
356
|
+
if (udt === "void") return "undefined";
|
|
357
|
+
if (prop?.enum?.length) return prop.enum.map(literal).join(" | ");
|
|
358
|
+
const t = Array.isArray(prop?.type) ? null : prop?.type;
|
|
359
|
+
if (t === "number" || t === "integer") return "number";
|
|
360
|
+
if (t === "boolean") return "boolean";
|
|
361
|
+
if (t === "string") return "string";
|
|
362
|
+
if (t === "array") return "unknown[]";
|
|
363
|
+
return "unknown";
|
|
364
|
+
}
|
|
365
|
+
function generateTypes(spec) {
|
|
366
|
+
const schemas = spec.components?.schemas ?? {};
|
|
367
|
+
const enums = spec.components?.["x-pg-enums"] ?? {};
|
|
368
|
+
const tableNames = Object.keys(schemas).sort();
|
|
369
|
+
let legacySpec = false;
|
|
370
|
+
const tableBlocks = [];
|
|
371
|
+
for (const table2 of tableNames) {
|
|
372
|
+
const schema = schemas[table2];
|
|
373
|
+
const required = new Set(schema.required ?? []);
|
|
374
|
+
const row = [];
|
|
375
|
+
const insert = [];
|
|
376
|
+
const update = [];
|
|
377
|
+
for (const [col, prop] of Object.entries(schema.properties ?? {})) {
|
|
378
|
+
if (!("x-pg-type" in prop)) legacySpec = true;
|
|
379
|
+
const nullable = prop.nullable === true || !required.has(col);
|
|
380
|
+
const udt = udtOf(prop);
|
|
381
|
+
const readType = tsType(udt, enums, false, prop) + (nullable ? " | null" : "");
|
|
382
|
+
const writeType = tsType(udt, enums, true, prop) + (nullable ? " | null" : "");
|
|
383
|
+
const neverWritten = prop["x-pg-generated"] === true || prop["x-pg-identity"] === "always";
|
|
384
|
+
const optionalOnInsert = nullable || prop["x-pg-has-default"] === true;
|
|
385
|
+
row.push(` ${key(col)}: ${readType}`);
|
|
386
|
+
if (neverWritten) {
|
|
387
|
+
insert.push(` ${key(col)}?: never`);
|
|
388
|
+
update.push(` ${key(col)}?: never`);
|
|
389
|
+
} else {
|
|
390
|
+
insert.push(` ${key(col)}${optionalOnInsert ? "?" : ""}: ${writeType}`);
|
|
391
|
+
update.push(` ${key(col)}?: ${writeType}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const body = (lines) => lines.length ? `{
|
|
395
|
+
${lines.join("\n")}
|
|
396
|
+
}` : "Record<string, never>";
|
|
397
|
+
tableBlocks.push(
|
|
398
|
+
` ${key(table2)}: {
|
|
399
|
+
Row: ${body(row)}
|
|
400
|
+
Insert: ${body(insert)}
|
|
401
|
+
Update: ${body(update)}
|
|
402
|
+
}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const functionBlocks = [];
|
|
406
|
+
const fnEntries = Object.entries(spec.paths ?? {}).map(([path, ops]) => ({ name: path.match(/\/studio\/rpc\/([^/]+)$/)?.[1], op: ops.post })).filter((f) => !!f.name && !!f.op).filter((f) => !["trigger", "event_trigger"].includes(f.op["x-pg-returns"]?.type ?? "")).sort((a, b) => a.name.localeCompare(b.name));
|
|
407
|
+
for (const { name, op } of fnEntries) {
|
|
408
|
+
const props = op.requestBody?.content?.["application/json"]?.schema?.properties ?? {};
|
|
409
|
+
const args = Object.entries(props).map(([arg, prop]) => {
|
|
410
|
+
const optional = prop["x-pg-has-default"] === true;
|
|
411
|
+
return ` ${key(arg)}${optional ? "?" : ""}: ${tsType(udtOf(prop), enums, true, prop)}`;
|
|
412
|
+
});
|
|
413
|
+
const ret = op["x-pg-returns"];
|
|
414
|
+
let returns;
|
|
415
|
+
if (!ret) {
|
|
416
|
+
legacySpec = true;
|
|
417
|
+
returns = "unknown";
|
|
418
|
+
} else {
|
|
419
|
+
let single;
|
|
420
|
+
if (ret.columns?.length) {
|
|
421
|
+
single = `{
|
|
422
|
+
${ret.columns.map((c) => ` ${key(c.name)}: ${tsType(c.type, enums, false)} | null`).join("\n")}
|
|
423
|
+
}`;
|
|
424
|
+
} else if (ret.type && schemas[ret.type]) {
|
|
425
|
+
single = `Database["public"]["Tables"][${literal(ret.type)}]["Row"]`;
|
|
426
|
+
} else {
|
|
427
|
+
single = tsType(ret.type, enums, false);
|
|
428
|
+
}
|
|
429
|
+
returns = ret.set ? /[|\s]/.test(single) && !single.startsWith("{") ? `(${single})[]` : `${single}[]` : single;
|
|
430
|
+
}
|
|
431
|
+
functionBlocks.push(
|
|
432
|
+
` ${key(name)}: {
|
|
433
|
+
Args: ${args.length ? `{
|
|
434
|
+
${args.join("\n")}
|
|
435
|
+
}` : "Record<PropertyKey, never>"}
|
|
436
|
+
Returns: ${returns}
|
|
437
|
+
}`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
const enumBlocks = Object.keys(enums).sort().map(
|
|
441
|
+
(name) => ` ${key(name)}: ${enums[name].length ? enums[name].map(literal).join(" | ") : "never"}`
|
|
442
|
+
);
|
|
443
|
+
const section = (blocks) => blocks.length ? `{
|
|
444
|
+
${blocks.join("\n")}
|
|
445
|
+
}` : "{\n [_ in never]: never\n }";
|
|
446
|
+
const source = `// Generated by \`deploro gen types\`. Do not edit by hand; re-run after each migration.
|
|
447
|
+
//
|
|
448
|
+
// bigint, numeric and money columns are strings in Row: Deploro returns them as
|
|
449
|
+
// strings so large values keep their precision. Insert and Update accept either.
|
|
450
|
+
|
|
451
|
+
export type Json =
|
|
452
|
+
| string
|
|
453
|
+
| number
|
|
454
|
+
| boolean
|
|
455
|
+
| null
|
|
456
|
+
| { [key: string]: Json | undefined }
|
|
457
|
+
| Json[]
|
|
458
|
+
|
|
459
|
+
export type Database = {
|
|
460
|
+
public: {
|
|
461
|
+
Tables: ${section(tableBlocks)}
|
|
462
|
+
Functions: ${section(functionBlocks)}
|
|
463
|
+
Enums: ${section(enumBlocks)}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
type PublicSchema = Database["public"]
|
|
468
|
+
|
|
469
|
+
export type Tables<T extends keyof PublicSchema["Tables"]> = PublicSchema["Tables"][T]["Row"]
|
|
470
|
+
export type TablesInsert<T extends keyof PublicSchema["Tables"]> = PublicSchema["Tables"][T]["Insert"]
|
|
471
|
+
export type TablesUpdate<T extends keyof PublicSchema["Tables"]> = PublicSchema["Tables"][T]["Update"]
|
|
472
|
+
export type Enums<T extends keyof PublicSchema["Enums"]> = PublicSchema["Enums"][T]
|
|
473
|
+
export type FunctionArgs<T extends keyof PublicSchema["Functions"]> = PublicSchema["Functions"][T]["Args"]
|
|
474
|
+
export type FunctionReturns<T extends keyof PublicSchema["Functions"]> = PublicSchema["Functions"][T]["Returns"]
|
|
475
|
+
`;
|
|
476
|
+
return {
|
|
477
|
+
source,
|
|
478
|
+
tables: tableNames.length,
|
|
479
|
+
functions: fnEntries.length,
|
|
480
|
+
enums: enumBlocks.length,
|
|
481
|
+
legacySpec
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/commands/gen.ts
|
|
486
|
+
async function genTypesCommand(opts) {
|
|
487
|
+
const config = requireProject();
|
|
488
|
+
const spec = await apiRequest(`/api/projects/${config.projectId}/studio/spec`);
|
|
489
|
+
const result = generateTypes(spec);
|
|
490
|
+
if (opts.out) {
|
|
491
|
+
mkdirSync3(dirname3(opts.out) || ".", { recursive: true });
|
|
492
|
+
writeFileSync3(opts.out, result.source, "utf8");
|
|
493
|
+
console.error(
|
|
494
|
+
`Wrote ${opts.out}: ${result.tables} table(s), ${result.functions} function(s), ${result.enums} enum(s)`
|
|
495
|
+
);
|
|
496
|
+
} else {
|
|
497
|
+
process.stdout.write(result.source);
|
|
498
|
+
}
|
|
499
|
+
if (result.legacySpec) {
|
|
500
|
+
console.error(
|
|
501
|
+
"Warning: this server's spec has no column defaults or function return types, so every NOT NULL column is required in Insert and function Returns are `unknown`. They'll be exact once the server is updated."
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
271
506
|
// src/lib/confirm.ts
|
|
272
507
|
import { createInterface as createInterface2 } from "readline";
|
|
273
508
|
async function confirmOrDie(question, yes) {
|
|
@@ -359,9 +594,10 @@ async function dbTablesCommand() {
|
|
|
359
594
|
}
|
|
360
595
|
async function dbQueryCommand(sql, opts) {
|
|
361
596
|
requireProject();
|
|
597
|
+
const params = opts.param?.length ? opts.param : void 0;
|
|
362
598
|
const data = await apiRequest(
|
|
363
599
|
"/api/db/query",
|
|
364
|
-
{ method: "POST", body: JSON.stringify({ sql }) }
|
|
600
|
+
{ method: "POST", body: JSON.stringify({ sql, params }) }
|
|
365
601
|
);
|
|
366
602
|
if (opts.json) {
|
|
367
603
|
json(data.rows);
|
|
@@ -593,6 +829,23 @@ async function tokenRevokeCommand(id, opts) {
|
|
|
593
829
|
await apiRequestNoProject(`/api/auth/tokens/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
594
830
|
console.log(`\u2713 Token ${id} revoked`);
|
|
595
831
|
}
|
|
832
|
+
async function tokenRotateCommand(id, opts) {
|
|
833
|
+
const config = requireConfig();
|
|
834
|
+
requireSessionAuth(config);
|
|
835
|
+
const graceHours = Number(opts.graceHours ?? 0);
|
|
836
|
+
const body = { graceHours };
|
|
837
|
+
if (opts.days !== void 0) body.expiresInDays = Number(opts.days);
|
|
838
|
+
const data = await apiRequestNoProject(
|
|
839
|
+
`/api/auth/tokens/${encodeURIComponent(id)}/rotate`,
|
|
840
|
+
{ method: "POST", body: JSON.stringify(body) }
|
|
841
|
+
);
|
|
842
|
+
console.log("\u2713 Token rotated. Copy the new one now, it will not be shown again:");
|
|
843
|
+
console.log(`
|
|
844
|
+
${data.token}
|
|
845
|
+
`);
|
|
846
|
+
console.log(` Expires : in ${data.expiresInDays} days`);
|
|
847
|
+
console.log(data.oldTokenValidForHours ? ` The old token keeps working for ${data.oldTokenValidForHours} hour(s).` : " The old token no longer works.");
|
|
848
|
+
}
|
|
596
849
|
function isEmailProvider(provider) {
|
|
597
850
|
return provider === "email" || provider === "email_password";
|
|
598
851
|
}
|
|
@@ -886,8 +1139,273 @@ async function resolveDomainId(projectId, idOrHostname) {
|
|
|
886
1139
|
return match.id;
|
|
887
1140
|
}
|
|
888
1141
|
|
|
1142
|
+
// src/commands/zones.ts
|
|
1143
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
1144
|
+
function zonesPath() {
|
|
1145
|
+
return `/api/projects/${requireProject().projectId}/zones`;
|
|
1146
|
+
}
|
|
1147
|
+
async function findZone(name) {
|
|
1148
|
+
const { zones: zones2 } = await apiRequest(zonesPath());
|
|
1149
|
+
const n = name.trim().toLowerCase();
|
|
1150
|
+
const match = zones2.filter((z) => n === z.name || n.endsWith(`.${z.name}`)).sort((a, b) => b.name.length - a.name.length)[0];
|
|
1151
|
+
if (!match) {
|
|
1152
|
+
console.error(`This project has no managed domain matching "${name}". Run: deploro zones list`);
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
return match;
|
|
1156
|
+
}
|
|
1157
|
+
function printNameservers(z) {
|
|
1158
|
+
console.log(`
|
|
1159
|
+
At your registrar, replace every nameserver for ${z.name} with:
|
|
1160
|
+
`);
|
|
1161
|
+
for (const ns of z.name_servers) console.log(` ${ns}`);
|
|
1162
|
+
if (z.original_name_servers.length) console.log(`
|
|
1163
|
+
(currently: ${z.original_name_servers.join(", ")})`);
|
|
1164
|
+
console.log(`
|
|
1165
|
+
Then run: deploro zones check ${z.name}`);
|
|
1166
|
+
}
|
|
1167
|
+
async function zonesListCommand() {
|
|
1168
|
+
const { zones: zones2 } = await apiRequest(zonesPath());
|
|
1169
|
+
if (!zones2.length) {
|
|
1170
|
+
console.log("This project has no managed domains. Add one with: deploro zones add <domain>");
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
table(zones2.map((z) => ({ domain: z.name, status: z.status, serving: z.hostname_count ?? 0 })));
|
|
1174
|
+
}
|
|
1175
|
+
async function zonesAddCommand(domain) {
|
|
1176
|
+
const data = await apiRequest(zonesPath(), { method: "POST", body: JSON.stringify({ name: domain }) });
|
|
1177
|
+
console.log(`\u2713 ${data.zone.name} added.
|
|
1178
|
+
`);
|
|
1179
|
+
console.log(`Copied ${data.imported_records.length} existing record(s) \u2014 check nothing you use is missing:`);
|
|
1180
|
+
for (const r of data.imported_records) console.log(` ${r.type.padEnd(6)} ${r.name} \u2192 ${r.priority ?? ""} ${r.content}`);
|
|
1181
|
+
for (const f of data.import_failed ?? []) console.log(` \u2717 ${f.record.type} ${f.record.name}: ${f.error}`);
|
|
1182
|
+
console.log("DNS can't list every name a domain has \u2014 add anything else with 'deploro zones records add' before switching.");
|
|
1183
|
+
if (data.legacy_hostnames.length) {
|
|
1184
|
+
console.log(`
|
|
1185
|
+
Already connected via CNAME: ${data.legacy_hostnames.join(", ")} \u2014 move them over with 'deploro zones attach' once live.`);
|
|
1186
|
+
}
|
|
1187
|
+
printNameservers(data.zone);
|
|
1188
|
+
}
|
|
1189
|
+
async function zonesStatusCommand(domain) {
|
|
1190
|
+
const z = await findZone(domain);
|
|
1191
|
+
const { zone } = await apiRequest(`${zonesPath()}/${z.id}`);
|
|
1192
|
+
console.log(`${zone.name}: ${zone.status}`);
|
|
1193
|
+
if (zone.status !== "active") printNameservers(zone);
|
|
1194
|
+
}
|
|
1195
|
+
async function zonesCheckCommand(domain) {
|
|
1196
|
+
const z = await findZone(domain);
|
|
1197
|
+
const { zone } = await apiRequest(`${zonesPath()}/${z.id}/check`, { method: "POST" });
|
|
1198
|
+
if (zone.status === "active") console.log(`\u2713 ${zone.name} is live on Deploro`);
|
|
1199
|
+
else console.log(`Not switched yet \u2014 nameserver changes can take a few hours. Deploro keeps checking and emails you when it's live.`);
|
|
1200
|
+
}
|
|
1201
|
+
async function zonesRemoveCommand(domain, opts) {
|
|
1202
|
+
const z = await findZone(domain);
|
|
1203
|
+
await confirmOrDie(
|
|
1204
|
+
`Remove ${z.name}? If its nameservers still point at Deploro, its website AND email stop resolving immediately.`,
|
|
1205
|
+
opts.yes
|
|
1206
|
+
);
|
|
1207
|
+
const data = await apiRequest(`${zonesPath()}/${z.id}`, {
|
|
1208
|
+
method: "DELETE",
|
|
1209
|
+
body: JSON.stringify({ confirm: z.name })
|
|
1210
|
+
});
|
|
1211
|
+
if (data.zone_file) {
|
|
1212
|
+
const file = `${z.name}.zone.txt`;
|
|
1213
|
+
writeFileSync4(file, data.zone_file);
|
|
1214
|
+
console.log(`Saved its records to ${file}`);
|
|
1215
|
+
}
|
|
1216
|
+
console.log(`\u2713 ${z.name} removed`);
|
|
1217
|
+
}
|
|
1218
|
+
async function zonesRecordsListCommand(domain) {
|
|
1219
|
+
const z = await findZone(domain);
|
|
1220
|
+
const { records } = await apiRequest(`${zonesPath()}/${z.id}/records`);
|
|
1221
|
+
table(
|
|
1222
|
+
records.map((r) => ({
|
|
1223
|
+
id: r.id,
|
|
1224
|
+
type: r.type,
|
|
1225
|
+
name: r.name,
|
|
1226
|
+
content: r.priority !== null && r.type === "MX" ? `${r.priority} ${r.content}` : r.content,
|
|
1227
|
+
proxied: r.proxied ? "yes" : "",
|
|
1228
|
+
managed: r.managed_by ?? ""
|
|
1229
|
+
}))
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
async function zonesRecordsAddCommand(domain, type, name, content, opts) {
|
|
1233
|
+
const z = await findZone(domain);
|
|
1234
|
+
const { record } = await apiRequest(`${zonesPath()}/${z.id}/records`, {
|
|
1235
|
+
method: "POST",
|
|
1236
|
+
body: JSON.stringify({
|
|
1237
|
+
type,
|
|
1238
|
+
name,
|
|
1239
|
+
content,
|
|
1240
|
+
...opts.ttl ? { ttl: Number(opts.ttl) } : {},
|
|
1241
|
+
...opts.priority ? { priority: Number(opts.priority) } : {},
|
|
1242
|
+
...opts.proxied ? { proxied: true } : {}
|
|
1243
|
+
})
|
|
1244
|
+
});
|
|
1245
|
+
console.log(`\u2713 Added ${record.type} ${record.name} \u2192 ${record.content} (id: ${record.id})`);
|
|
1246
|
+
}
|
|
1247
|
+
async function zonesRecordsRemoveCommand(domain, recordId, opts) {
|
|
1248
|
+
const z = await findZone(domain);
|
|
1249
|
+
await confirmOrDie(`Delete record ${recordId} from ${z.name}?`, opts.yes);
|
|
1250
|
+
await apiRequest(`${zonesPath()}/${z.id}/records/${recordId}`, { method: "DELETE" });
|
|
1251
|
+
console.log("\u2713 Record deleted");
|
|
1252
|
+
}
|
|
1253
|
+
async function zonesAttachCommand(hostname, opts) {
|
|
1254
|
+
const z = await findZone(hostname);
|
|
1255
|
+
const h = hostname.trim().toLowerCase();
|
|
1256
|
+
try {
|
|
1257
|
+
const data = await apiRequest(
|
|
1258
|
+
`${zonesPath()}/${z.id}/hostnames`,
|
|
1259
|
+
{ method: "POST", body: JSON.stringify({ hostname: h, replace: !!opts.replace }) }
|
|
1260
|
+
);
|
|
1261
|
+
if (data.attached) console.log(`\u2713 https://${data.hostname.hostname} now serves this project`);
|
|
1262
|
+
else console.log(`Saved, but not attached yet: ${data.error}`);
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
const msg = err.message;
|
|
1265
|
+
if (msg.includes("already points somewhere")) {
|
|
1266
|
+
console.error(`${h} already has DNS records or a CNAME connection. Re-run with --replace to point it at this project instead.`);
|
|
1267
|
+
process.exit(1);
|
|
1268
|
+
}
|
|
1269
|
+
throw err;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
async function zonesDetachCommand(hostname, opts) {
|
|
1273
|
+
const z = await findZone(hostname);
|
|
1274
|
+
const h = hostname.trim().toLowerCase();
|
|
1275
|
+
const { hostnames } = await apiRequest(`${zonesPath()}/${z.id}/hostnames`);
|
|
1276
|
+
const match = hostnames.find((x) => x.hostname === h);
|
|
1277
|
+
if (!match) {
|
|
1278
|
+
console.error(`${h} isn't serving this project.`);
|
|
1279
|
+
process.exit(1);
|
|
1280
|
+
}
|
|
1281
|
+
await confirmOrDie(`Stop serving this project on ${h}? It stops immediately.`, opts.yes);
|
|
1282
|
+
await apiRequest(`${zonesPath()}/${z.id}/hostnames/${match.id}`, { method: "DELETE" });
|
|
1283
|
+
console.log(`\u2713 ${h} detached`);
|
|
1284
|
+
}
|
|
1285
|
+
var FORWARDING = "cf_routing";
|
|
1286
|
+
async function zonesEmailStatusCommand(domain) {
|
|
1287
|
+
const z = await findZone(domain);
|
|
1288
|
+
const s = await apiRequest(`${zonesPath()}/${z.id}/email`);
|
|
1289
|
+
const names = new Map(s.presets.map((p) => [p.kind, p.name]));
|
|
1290
|
+
const active = s.services.map((x) => x.kind === FORWARDING ? "Forwarding" : names.get(x.kind) ?? x.kind);
|
|
1291
|
+
console.log(`Email on ${z.name}: ${active.length ? active.join(", ") : "nothing set up"}`);
|
|
1292
|
+
if (s.forwarding.enabled) {
|
|
1293
|
+
console.log("\nForwarding:");
|
|
1294
|
+
for (const r of s.forwarding.rules) {
|
|
1295
|
+
console.log(` ${r.address} \u2192 ${r.destination}${r.enabled ? "" : " (paused until the inbox confirms)"}`);
|
|
1296
|
+
}
|
|
1297
|
+
const ca = s.forwarding.catch_all;
|
|
1298
|
+
console.log(` everything else \u2192 ${ca?.enabled ? ca.destination : "rejected"}`);
|
|
1299
|
+
}
|
|
1300
|
+
if (s.destinations.length) {
|
|
1301
|
+
console.log("\nInboxes:");
|
|
1302
|
+
for (const d of s.destinations) {
|
|
1303
|
+
const state = d.usable ? "confirmed" : `waiting for them to click the ${d.waiting_for === "deploro" ? "Deploro" : "Cloudflare"} email`;
|
|
1304
|
+
console.log(` ${d.email} ${state}`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
console.log(`
|
|
1308
|
+
Providers: ${s.presets.map((p) => p.kind).join(", ")} \u2014 or 'deploro zones email-forwarding on ${z.name}'`);
|
|
1309
|
+
}
|
|
1310
|
+
async function zonesEmailSetupCommand(domain, provider, opts) {
|
|
1311
|
+
const z = await findZone(domain);
|
|
1312
|
+
const config = {};
|
|
1313
|
+
for (const kv of opts.set ?? []) {
|
|
1314
|
+
const i = kv.indexOf("=");
|
|
1315
|
+
if (i < 1) {
|
|
1316
|
+
console.error(`--set expects key=value, got "${kv}"`);
|
|
1317
|
+
process.exit(1);
|
|
1318
|
+
}
|
|
1319
|
+
config[kv.slice(0, i)] = kv.slice(i + 1);
|
|
1320
|
+
}
|
|
1321
|
+
try {
|
|
1322
|
+
await apiRequest(`${zonesPath()}/${z.id}/email/presets/${provider}`, {
|
|
1323
|
+
method: "POST",
|
|
1324
|
+
body: JSON.stringify({ config, replace: !!opts.replace })
|
|
1325
|
+
});
|
|
1326
|
+
console.log(`\u2713 ${provider} is set up on ${z.name}`);
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
if (err.message.includes("replaces existing records")) {
|
|
1329
|
+
console.error(
|
|
1330
|
+
`${z.name} already has mail records this would replace \u2014 see 'deploro zones records list ${z.name}'. Re-run with --replace to replace them.`
|
|
1331
|
+
);
|
|
1332
|
+
process.exit(1);
|
|
1333
|
+
}
|
|
1334
|
+
throw err;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async function zonesEmailRemoveCommand(domain, provider, opts) {
|
|
1338
|
+
const z = await findZone(domain);
|
|
1339
|
+
await confirmOrDie(`Remove ${provider} from ${z.name}? Its DNS records are deleted and mail stops arriving there.`, opts.yes);
|
|
1340
|
+
await apiRequest(`${zonesPath()}/${z.id}/email/presets/${provider}`, { method: "DELETE" });
|
|
1341
|
+
console.log(`\u2713 ${provider} removed`);
|
|
1342
|
+
}
|
|
1343
|
+
async function zonesEmailForwardingCommand(mode, domain, opts) {
|
|
1344
|
+
const z = await findZone(domain);
|
|
1345
|
+
if (mode === "on") {
|
|
1346
|
+
try {
|
|
1347
|
+
await apiRequest(`${zonesPath()}/${z.id}/email/forwarding`, {
|
|
1348
|
+
method: "POST",
|
|
1349
|
+
body: JSON.stringify({ replace: !!opts.replace })
|
|
1350
|
+
});
|
|
1351
|
+
} catch (err) {
|
|
1352
|
+
if (err.message.includes("replaces the domain's existing mail servers")) {
|
|
1353
|
+
console.error(`${z.name} already has MX records. Re-run with --replace to replace them with forwarding.`);
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
}
|
|
1356
|
+
throw err;
|
|
1357
|
+
}
|
|
1358
|
+
console.log(`\u2713 Forwarding is on for ${z.name}. Next: deploro zones forward hello@${z.name} you@gmail.com`);
|
|
1359
|
+
} else if (mode === "off") {
|
|
1360
|
+
await confirmOrDie(`Turn off forwarding for ${z.name}? Mail to it will bounce.`, opts.yes);
|
|
1361
|
+
await apiRequest(`${zonesPath()}/${z.id}/email/forwarding`, { method: "DELETE" });
|
|
1362
|
+
console.log("\u2713 Forwarding is off");
|
|
1363
|
+
} else {
|
|
1364
|
+
console.error("Use: deploro zones email-forwarding on|off <domain>");
|
|
1365
|
+
process.exit(1);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
async function zonesForwardCommand(address, inbox) {
|
|
1369
|
+
const [local, domain] = address.toLowerCase().split("@");
|
|
1370
|
+
if (!local || !domain) {
|
|
1371
|
+
console.error("The first argument is the address to forward, like hello@yourdomain.com");
|
|
1372
|
+
process.exit(1);
|
|
1373
|
+
}
|
|
1374
|
+
const z = await findZone(domain);
|
|
1375
|
+
const { projectId } = requireProject();
|
|
1376
|
+
const email = inbox.trim().toLowerCase();
|
|
1377
|
+
let s = await apiRequest(`${zonesPath()}/${z.id}/email`);
|
|
1378
|
+
let dest = s.destinations.find((d) => d.email === email);
|
|
1379
|
+
if (!dest) {
|
|
1380
|
+
await apiRequest(`/api/projects/${projectId}/email-destinations`, { method: "POST", body: JSON.stringify({ email }) });
|
|
1381
|
+
s = await apiRequest(`${zonesPath()}/${z.id}/email`);
|
|
1382
|
+
dest = s.destinations.find((d) => d.email === email);
|
|
1383
|
+
}
|
|
1384
|
+
if (!dest?.usable) {
|
|
1385
|
+
console.log(`${email} has been sent a confirmation email. Once they click it, run this command again.`);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
await apiRequest(`${zonesPath()}/${z.id}/email/rules`, {
|
|
1389
|
+
method: "POST",
|
|
1390
|
+
body: JSON.stringify({ local, destination_id: dest.id })
|
|
1391
|
+
});
|
|
1392
|
+
console.log(`\u2713 ${local}@${z.name} \u2192 ${email}`);
|
|
1393
|
+
}
|
|
1394
|
+
async function zonesUnforwardCommand(address) {
|
|
1395
|
+
const domain = address.toLowerCase().split("@")[1] ?? "";
|
|
1396
|
+
const z = await findZone(domain);
|
|
1397
|
+
const s = await apiRequest(`${zonesPath()}/${z.id}/email`);
|
|
1398
|
+
const rule = s.forwarding.rules.find((r) => r.address === address.toLowerCase());
|
|
1399
|
+
if (!rule) {
|
|
1400
|
+
console.error(`${address} isn't forwarded.`);
|
|
1401
|
+
process.exit(1);
|
|
1402
|
+
}
|
|
1403
|
+
await apiRequest(`${zonesPath()}/${z.id}/email/rules/${rule.id}`, { method: "DELETE" });
|
|
1404
|
+
console.log(`\u2713 ${address} no longer forwards`);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
889
1407
|
// src/commands/storage.ts
|
|
890
|
-
import { readFileSync as readFileSync2, writeFileSync as
|
|
1408
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
891
1409
|
import { basename } from "path";
|
|
892
1410
|
function authHeaders2(config) {
|
|
893
1411
|
return { Authorization: `Bearer ${config.accessToken}` };
|
|
@@ -930,27 +1448,27 @@ async function storageUploadCommand(filePath, opts) {
|
|
|
930
1448
|
const data = await res.json();
|
|
931
1449
|
console.log(`\u2713 Uploaded as ${data.key} (${data.size} bytes, ${data.contentType})`);
|
|
932
1450
|
}
|
|
933
|
-
async function storageDownloadCommand(
|
|
1451
|
+
async function storageDownloadCommand(key2, opts) {
|
|
934
1452
|
const config = requireProject();
|
|
935
1453
|
const res = await fetch(
|
|
936
|
-
`${config.baseUrl}/api/projects/${config.projectId}/storage/download?key=${encodeURIComponent(
|
|
1454
|
+
`${config.baseUrl}/api/projects/${config.projectId}/storage/download?key=${encodeURIComponent(key2)}`,
|
|
937
1455
|
{ headers: authHeaders2(config) }
|
|
938
1456
|
);
|
|
939
1457
|
await throwOnError2(res);
|
|
940
1458
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
941
|
-
const outPath = opts.out ?? basename(
|
|
942
|
-
|
|
1459
|
+
const outPath = opts.out ?? basename(key2);
|
|
1460
|
+
writeFileSync5(outPath, bytes);
|
|
943
1461
|
console.log(`\u2713 Saved to ${outPath} (${bytes.length} bytes)`);
|
|
944
1462
|
}
|
|
945
|
-
async function storageDeleteCommand(
|
|
1463
|
+
async function storageDeleteCommand(key2, opts) {
|
|
946
1464
|
const config = requireProject();
|
|
947
|
-
await confirmOrDie(`Delete "${
|
|
1465
|
+
await confirmOrDie(`Delete "${key2}" from storage?`, opts.yes);
|
|
948
1466
|
const res = await fetch(
|
|
949
|
-
`${config.baseUrl}/api/projects/${config.projectId}/storage?key=${encodeURIComponent(
|
|
1467
|
+
`${config.baseUrl}/api/projects/${config.projectId}/storage?key=${encodeURIComponent(key2)}`,
|
|
950
1468
|
{ method: "DELETE", headers: authHeaders2(config) }
|
|
951
1469
|
);
|
|
952
1470
|
await throwOnError2(res);
|
|
953
|
-
console.log(`\u2713 Deleted ${
|
|
1471
|
+
console.log(`\u2713 Deleted ${key2}`);
|
|
954
1472
|
}
|
|
955
1473
|
async function storagePublicCommand(mode, opts) {
|
|
956
1474
|
const config = requireProject();
|
|
@@ -1052,13 +1570,13 @@ function parseDuration(input) {
|
|
|
1052
1570
|
const mult = { s: 1, m: 60, h: 3600, d: 86400 }[unit] ?? 1;
|
|
1053
1571
|
return n * mult;
|
|
1054
1572
|
}
|
|
1055
|
-
async function storageSignCommand(
|
|
1573
|
+
async function storageSignCommand(key2, opts) {
|
|
1056
1574
|
const config = requireProject();
|
|
1057
1575
|
const expiresIn = opts.expires ? parseDuration(opts.expires) : void 0;
|
|
1058
1576
|
const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}/storage/signed-url`, {
|
|
1059
1577
|
method: "POST",
|
|
1060
1578
|
headers: { ...authHeaders2(config), "Content-Type": "application/json" },
|
|
1061
|
-
body: JSON.stringify({ key, expiresIn })
|
|
1579
|
+
body: JSON.stringify({ key: key2, expiresIn })
|
|
1062
1580
|
});
|
|
1063
1581
|
await throwOnError2(res);
|
|
1064
1582
|
const data = await res.json();
|
|
@@ -1315,13 +1833,13 @@ async function hostingStatusCommand() {
|
|
|
1315
1833
|
}
|
|
1316
1834
|
console.log("");
|
|
1317
1835
|
}
|
|
1318
|
-
async function hostingSetEnvCommand(
|
|
1836
|
+
async function hostingSetEnvCommand(key2, value) {
|
|
1319
1837
|
requireConfig();
|
|
1320
1838
|
await apiRequest("/api/hosting/env", {
|
|
1321
1839
|
method: "POST",
|
|
1322
|
-
body: JSON.stringify({ key, value })
|
|
1840
|
+
body: JSON.stringify({ key: key2, value })
|
|
1323
1841
|
});
|
|
1324
|
-
console.log(`\u2713 Secret '${
|
|
1842
|
+
console.log(`\u2713 Secret '${key2}' updated`);
|
|
1325
1843
|
}
|
|
1326
1844
|
async function hostingEnvCommand() {
|
|
1327
1845
|
requireConfig();
|
|
@@ -1659,13 +2177,13 @@ async function vpsEnvCommand() {
|
|
|
1659
2177
|
for (const v of data.vars) console.log(` ${v.key}`);
|
|
1660
2178
|
console.log("");
|
|
1661
2179
|
}
|
|
1662
|
-
async function vpsSetEnvCommand(
|
|
2180
|
+
async function vpsSetEnvCommand(key2, value) {
|
|
1663
2181
|
const config = requireProject();
|
|
1664
2182
|
await apiRequest(`/api/projects/${config.projectId}/vps/env`, {
|
|
1665
2183
|
method: "POST",
|
|
1666
|
-
body: JSON.stringify({ key, value })
|
|
2184
|
+
body: JSON.stringify({ key: key2, value })
|
|
1667
2185
|
});
|
|
1668
|
-
console.log(`\u2713 VPS env var '${
|
|
2186
|
+
console.log(`\u2713 VPS env var '${key2}' set \u2014 run 'deploro vps deploy' to apply it`);
|
|
1669
2187
|
}
|
|
1670
2188
|
async function vpsDeployCommand() {
|
|
1671
2189
|
const config = requireProject();
|
|
@@ -1803,12 +2321,14 @@ program.command("create <name>").description("Create a new project (and set it a
|
|
|
1803
2321
|
program.command("use <project>").description("Set the active project (accepts id, slug, or name)").action(useProjectCommand);
|
|
1804
2322
|
program.command("delete <project>").description("Delete a project and its database (accepts id, slug, or name)").option("-y, --yes", "Skip confirmation prompt").action(projectsDeleteCommand);
|
|
1805
2323
|
program.command("export").description("Export the active project's database and storage manifest (NDJSON)").option("--out <file>", "Where to write the export (default: deploro-export-<date>.ndjson)").option("--storage <dir>", "Also download stored files into this directory").option("--skip-storage", "Skip the object-storage listing entirely").action(exportCommand);
|
|
2324
|
+
var gen = program.command("gen").description("Generate code from the active project");
|
|
2325
|
+
gen.command("types").description("TypeScript types for the project's tables, functions and enums (prints to stdout)").option("--out <file>", "Write to this file instead of stdout").action(genTypesCommand);
|
|
1806
2326
|
var db = program.command("db").description("Database commands");
|
|
1807
2327
|
db.command("create").description("Provision the active project's database (idempotent)").action(dbCreateCommand);
|
|
1808
2328
|
db.command("info").description("Show database status and how to reach it from a deployed app").action(dbInfoCommand);
|
|
1809
2329
|
db.command("tables").description("List tables in the active project").action(dbTablesCommand);
|
|
1810
2330
|
db.command("rows <table>").description("List rows in a table").option("--limit <n>", "Rows per page (max 200)", "20").option("--page <n>", "Page number", "1").option("--json", "Output raw JSON").action(dbRowsCommand);
|
|
1811
|
-
db.command("query <sql>").description("Run a SQL query (project admin)").option("--json", "Output raw JSON").action(dbQueryCommand);
|
|
2331
|
+
db.command("query <sql>").description("Run a read-only SQL query (project admin)").option("--param <value>", "Bind a value to $1, $2, ... in order (repeatable)", (v, prev) => [...prev, v], []).option("--json", "Output raw JSON").action(dbQueryCommand);
|
|
1812
2332
|
db.command("schema <table>").description("Show schema for a table").action(dbSchemaCommand);
|
|
1813
2333
|
db.command("insert <table>").description("Insert a row").requiredOption("--data <json>", "Row data as a JSON object").action(dbInsertCommand);
|
|
1814
2334
|
db.command("update <table> <id>").description("Update a row by id").requiredOption("--data <json>", "Fields to update as a JSON object").action(dbUpdateCommand);
|
|
@@ -1840,6 +2360,7 @@ authEndUsers.command("delete <id>").description("Delete an end user and all thei
|
|
|
1840
2360
|
var token = program.command("token").description("Personal access token commands");
|
|
1841
2361
|
token.command("list").description("List your personal access tokens").action(tokenListCommand);
|
|
1842
2362
|
token.command("create <name>").description("Create a new project-scoped personal access token").option("--read", "Grant read scope").option("--write", "Grant write scope").option("--project <slugs>", "Comma-separated project slugs/ids/names (defaults to the active project)").option("--days <n>", "Expiry in days (1-365)", "90").action(tokenCreateCommand);
|
|
2363
|
+
token.command("rotate <id>").description("Replace a token with a new one (same name, scopes and projects)").option("--grace-hours <n>", "Keep the old token working for this many hours (0-72)", "0").option("--days <n>", "Expiry of the new token in days (1-365, default: the old token's lifetime)").action(tokenRotateCommand);
|
|
1843
2364
|
token.command("revoke <id>").description("Revoke a personal access token").option("--yes", "Skip confirmation prompt").action(tokenRevokeCommand);
|
|
1844
2365
|
var hosting = program.command("hosting").description("Hosting commands");
|
|
1845
2366
|
hosting.command("status").description("Show Worker and VPS health").action(hostingStatusCommand);
|
|
@@ -1889,6 +2410,24 @@ domains.command("add <hostname>").description("Add a domain \u2014 prints the CN
|
|
|
1889
2410
|
domains.command("verify <id-or-hostname>").description("Verify DNS ownership and register SSL").action(domainsVerifyCommand);
|
|
1890
2411
|
domains.command("status <id-or-hostname>").description("Poll domain verification/SSL status").action(domainsStatusCommand);
|
|
1891
2412
|
domains.command("remove <id-or-hostname>").description("Remove a domain").option("-y, --yes", "Skip confirmation prompt").action(domainsRemoveCommand);
|
|
2413
|
+
var zones = program.command("zones").description("The active project's domains whose DNS Deploro manages (switch nameservers once, then manage everything)");
|
|
2414
|
+
zones.command("list").description("List the project's managed domains").action(zonesListCommand);
|
|
2415
|
+
zones.command("add <domain>").description("Add a domain \u2014 copies its current records and prints the nameservers to switch to").action(zonesAddCommand);
|
|
2416
|
+
zones.command("status <domain>").description("Show whether the nameserver switch has taken effect").action(zonesStatusCommand);
|
|
2417
|
+
zones.command("check <domain>").description("Ask Deploro to re-check the nameservers now").action(zonesCheckCommand);
|
|
2418
|
+
zones.command("remove <domain>").description("Remove a domain (saves its records to a zone file first)").option("-y, --yes", "Skip confirmation prompt").action(zonesRemoveCommand);
|
|
2419
|
+
var zoneRecords = zones.command("records").description("DNS records of a managed domain");
|
|
2420
|
+
zoneRecords.command("list <domain>").description("List records").action(zonesRecordsListCommand);
|
|
2421
|
+
zoneRecords.command("add <domain> <type> <name> <content>").description("Add a record (name: @ for the root, or e.g. www)").option("--ttl <seconds>", "TTL (default auto)").option("--priority <n>", "MX priority (default 10)").option("--proxied", "Proxy through Cloudflare (A/AAAA/CNAME only)").action(zonesRecordsAddCommand);
|
|
2422
|
+
zoneRecords.command("rm <domain> <record-id>").description("Delete a record").option("-y, --yes", "Skip confirmation prompt").action(zonesRecordsRemoveCommand);
|
|
2423
|
+
zones.command("attach <hostname>").description("Serve the active project on a managed domain or subdomain").option("--replace", "Replace records or a CNAME connection already at that hostname").action(zonesAttachCommand);
|
|
2424
|
+
zones.command("detach <hostname>").description("Stop serving the active project on that hostname").option("-y, --yes", "Skip confirmation prompt").action(zonesDetachCommand);
|
|
2425
|
+
zones.command("email <domain>").description("Show email setup: providers, forwarding, inboxes").action(zonesEmailStatusCommand);
|
|
2426
|
+
zones.command("email-setup <domain> <provider>").description("Set up google_workspace | microsoft_365 | zoho | fastmail | resend").option("--set <key=value...>", "Provider values, e.g. --set region=eu dkim_selector=zmail").option("--replace", "Replace existing mail records").action(zonesEmailSetupCommand);
|
|
2427
|
+
zones.command("email-remove <domain> <provider>").description("Remove a provider and its records").option("-y, --yes", "Skip confirmation prompt").action(zonesEmailRemoveCommand);
|
|
2428
|
+
zones.command("email-forwarding <mode> <domain>").description("Turn free email forwarding on or off (mode: on|off)").option("--replace", "Replace existing MX records").option("-y, --yes", "Skip confirmation prompt").action(zonesEmailForwardingCommand);
|
|
2429
|
+
zones.command("forward <address> <inbox>").description("Forward e.g. hello@yourdomain.com to you@gmail.com (the inbox confirms once)").action(zonesForwardCommand);
|
|
2430
|
+
zones.command("unforward <address>").description("Stop forwarding an address").action(zonesUnforwardCommand);
|
|
1892
2431
|
var storage = program.command("storage").description("R2 file storage for the active project");
|
|
1893
2432
|
storage.command("list").description("List stored files").action(storageListCommand);
|
|
1894
2433
|
storage.command("upload <file>").description("Upload a local file").option("--key <name>", "Stored file name (defaults to the local file's name)").option("--bucket <name>", "Upload into an existing bucket instead of the project root").action(storageUploadCommand);
|
package/package.json
CHANGED