deploro 0.5.1 → 0.6.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.
Files changed (2) hide show
  1. package/dist/index.js +265 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -148,6 +148,88 @@ async function logoutCommand() {
148
148
  console.log("\u2713 Logged out \u2014 local credentials cleared");
149
149
  }
150
150
 
151
+ // src/commands/export.ts
152
+ import { createWriteStream, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
153
+ import { dirname as dirname2, join as join2 } from "path";
154
+ import { Readable } from "stream";
155
+ import { pipeline } from "stream/promises";
156
+ function authHeaders(config) {
157
+ return {
158
+ Authorization: `Bearer ${config.accessToken}`,
159
+ "x-project-id": config.projectId
160
+ };
161
+ }
162
+ async function throwOnError(res) {
163
+ if (res.ok) return;
164
+ const body = await res.json().catch(() => ({ error: res.statusText }));
165
+ throw new Error(`${res.status} ${body.error ?? res.statusText}`);
166
+ }
167
+ async function exportCommand(opts) {
168
+ const config = requireProject();
169
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
170
+ const outPath = opts.out ?? `deploro-export-${stamp}.ndjson`;
171
+ const url = new URL(`${config.baseUrl}/api/projects/${config.projectId}/export`);
172
+ if (opts.skipStorage) url.searchParams.set("storage", "false");
173
+ console.log(`Exporting to ${outPath} \u2026`);
174
+ const res = await fetch(url, { headers: authHeaders(config) });
175
+ await throwOnError(res);
176
+ if (!res.body) throw new Error("Export returned an empty response");
177
+ mkdirSync2(dirname2(outPath) || ".", { recursive: true });
178
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(outPath));
179
+ const { readFileSync: readFileSync4 } = await import("fs");
180
+ const lines = readFileSync4(outPath, "utf8").split("\n").filter(Boolean);
181
+ const last = lines.length ? JSON.parse(lines[lines.length - 1]) : null;
182
+ if (!last || last.type !== "end") {
183
+ throw new Error(
184
+ `Export did not finish cleanly \u2014 ${outPath} is incomplete (last record: ${last?.type ?? "none"}). Re-run it.`
185
+ );
186
+ }
187
+ console.log(`Wrote ${outPath}: ${last.rows ?? 0} rows across ${last.tables ?? 0} tables`);
188
+ if (last.errors?.length) {
189
+ console.log("\nSome parts could not be exported:");
190
+ for (const err of last.errors) console.log(` - ${err}`);
191
+ }
192
+ if (!opts.storage) {
193
+ const objects = lines.filter((l) => l.includes('"type":"storage"')).length;
194
+ if (objects > 0) {
195
+ console.log(
196
+ `
197
+ ${objects} stored file(s) are listed by key only. Re-run with --storage <dir> to download their contents.`
198
+ );
199
+ }
200
+ return;
201
+ }
202
+ const keys = lines.map((l) => JSON.parse(l)).filter((r) => r.type === "storage");
203
+ if (keys.length === 0) {
204
+ console.log("\nNo stored files to download.");
205
+ return;
206
+ }
207
+ console.log(`
208
+ Downloading ${keys.length} file(s) to ${opts.storage} \u2026`);
209
+ let downloaded = 0;
210
+ const failures = [];
211
+ for (const object of keys) {
212
+ const target = join2(opts.storage, object.key);
213
+ try {
214
+ const fileRes = await fetch(
215
+ `${config.baseUrl}/api/projects/${config.projectId}/storage/download?key=${encodeURIComponent(object.key)}`,
216
+ { headers: authHeaders(config) }
217
+ );
218
+ if (!fileRes.ok) throw new Error(`${fileRes.status} ${fileRes.statusText}`);
219
+ mkdirSync2(dirname2(target), { recursive: true });
220
+ writeFileSync2(target, Buffer.from(await fileRes.arrayBuffer()));
221
+ downloaded++;
222
+ } catch (err) {
223
+ failures.push(`${object.key}: ${err instanceof Error ? err.message : String(err)}`);
224
+ }
225
+ }
226
+ console.log(`Downloaded ${downloaded}/${keys.length} file(s).`);
227
+ if (failures.length) {
228
+ console.log("\nFailed:");
229
+ for (const failure of failures) console.log(` - ${failure}`);
230
+ }
231
+ }
232
+
151
233
  // src/lib/api.ts
152
234
  function errorMessage(status, body, statusText) {
153
235
  const parts = [body.error ?? statusText];
@@ -805,12 +887,12 @@ async function resolveDomainId(projectId, idOrHostname) {
805
887
  }
806
888
 
807
889
  // src/commands/storage.ts
808
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
890
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
809
891
  import { basename } from "path";
810
- function authHeaders(config) {
892
+ function authHeaders2(config) {
811
893
  return { Authorization: `Bearer ${config.accessToken}` };
812
894
  }
813
- async function throwOnError(res) {
895
+ async function throwOnError2(res) {
814
896
  if (res.ok) return;
815
897
  const body = await res.json().catch(() => ({ error: res.statusText }));
816
898
  throw new Error(`${res.status} ${body.error ?? res.statusText}`);
@@ -818,9 +900,9 @@ async function throwOnError(res) {
818
900
  async function storageListCommand() {
819
901
  const config = requireProject();
820
902
  const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}/storage`, {
821
- headers: authHeaders(config)
903
+ headers: authHeaders2(config)
822
904
  });
823
- await throwOnError(res);
905
+ await throwOnError2(res);
824
906
  const data = await res.json();
825
907
  table(
826
908
  data.objects.map((o) => ({
@@ -838,12 +920,13 @@ async function storageUploadCommand(filePath, opts) {
838
920
  const filename = opts.key ?? basename(filePath);
839
921
  const form = new FormData();
840
922
  form.append("file", new Blob([bytes]), filename);
923
+ if (opts.bucket) form.append("bucket", opts.bucket);
841
924
  const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}/storage/upload`, {
842
925
  method: "POST",
843
- headers: authHeaders(config),
926
+ headers: authHeaders2(config),
844
927
  body: form
845
928
  });
846
- await throwOnError(res);
929
+ await throwOnError2(res);
847
930
  const data = await res.json();
848
931
  console.log(`\u2713 Uploaded as ${data.key} (${data.size} bytes, ${data.contentType})`);
849
932
  }
@@ -851,12 +934,12 @@ async function storageDownloadCommand(key, opts) {
851
934
  const config = requireProject();
852
935
  const res = await fetch(
853
936
  `${config.baseUrl}/api/projects/${config.projectId}/storage/download?key=${encodeURIComponent(key)}`,
854
- { headers: authHeaders(config) }
937
+ { headers: authHeaders2(config) }
855
938
  );
856
- await throwOnError(res);
939
+ await throwOnError2(res);
857
940
  const bytes = new Uint8Array(await res.arrayBuffer());
858
941
  const outPath = opts.out ?? basename(key);
859
- writeFileSync2(outPath, bytes);
942
+ writeFileSync3(outPath, bytes);
860
943
  console.log(`\u2713 Saved to ${outPath} (${bytes.length} bytes)`);
861
944
  }
862
945
  async function storageDeleteCommand(key, opts) {
@@ -864,11 +947,124 @@ async function storageDeleteCommand(key, opts) {
864
947
  await confirmOrDie(`Delete "${key}" from storage?`, opts.yes);
865
948
  const res = await fetch(
866
949
  `${config.baseUrl}/api/projects/${config.projectId}/storage?key=${encodeURIComponent(key)}`,
867
- { method: "DELETE", headers: authHeaders(config) }
950
+ { method: "DELETE", headers: authHeaders2(config) }
868
951
  );
869
- await throwOnError(res);
952
+ await throwOnError2(res);
870
953
  console.log(`\u2713 Deleted ${key}`);
871
954
  }
955
+ async function storagePublicCommand(mode, opts) {
956
+ const config = requireProject();
957
+ const normalized = mode.toLowerCase();
958
+ if (!["on", "off", "status"].includes(normalized)) {
959
+ throw new Error(`Expected "on", "off", or "status" \u2014 got "${mode}"`);
960
+ }
961
+ const projectRes = await fetch(`${config.baseUrl}/api/projects/${config.projectId}`, {
962
+ headers: authHeaders2(config)
963
+ });
964
+ await throwOnError2(projectRes);
965
+ const project = await projectRes.json();
966
+ const enabled = Boolean(project.public_storage_enabled);
967
+ const publicBase = `${config.baseUrl}/files/${project.slug}/`;
968
+ if (normalized === "status") {
969
+ console.log(enabled ? `Public read is ON \u2014 ${publicBase}<key>` : "Public read is OFF");
970
+ return;
971
+ }
972
+ const target = normalized === "on";
973
+ if (target === enabled) {
974
+ console.log(`Public read is already ${target ? "on" : "off"} \u2014 nothing to do.`);
975
+ return;
976
+ }
977
+ if (target) {
978
+ await confirmOrDie(
979
+ `Make every file in "${project.slug}" readable by anyone who knows its key (no login)?`,
980
+ opts.yes
981
+ );
982
+ }
983
+ const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}`, {
984
+ method: "PATCH",
985
+ headers: { ...authHeaders2(config), "Content-Type": "application/json" },
986
+ body: JSON.stringify({ public_storage_enabled: target })
987
+ });
988
+ await throwOnError2(res);
989
+ console.log(target ? `\u2713 Public read ON \u2014 ${publicBase}<key>` : "\u2713 Public read OFF");
990
+ }
991
+ async function storageBucketsListCommand() {
992
+ const config = requireProject();
993
+ const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}/storage/buckets`, {
994
+ headers: authHeaders2(config)
995
+ });
996
+ await throwOnError2(res);
997
+ const data = await res.json();
998
+ if (!data.buckets.length) {
999
+ console.log("No buckets yet \u2014 files without a --bucket are stored at the project root.");
1000
+ return;
1001
+ }
1002
+ table(
1003
+ data.buckets.map((b) => ({
1004
+ name: b.name,
1005
+ access: b.public ? "public" : "private",
1006
+ created: new Date(b.created_at).toLocaleString()
1007
+ }))
1008
+ );
1009
+ }
1010
+ async function storageBucketsCreateCommand(name, opts) {
1011
+ const config = requireProject();
1012
+ const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}/storage/buckets`, {
1013
+ method: "POST",
1014
+ headers: { ...authHeaders2(config), "Content-Type": "application/json" },
1015
+ body: JSON.stringify({ name, public: Boolean(opts.public) })
1016
+ });
1017
+ await throwOnError2(res);
1018
+ const data = await res.json();
1019
+ console.log(`\u2713 Created bucket "${data.name}" (${data.public ? "public" : "private"}) \u2014 upload with --bucket ${data.name}`);
1020
+ }
1021
+ async function storageBucketsSetPublicCommand(name, mode, opts) {
1022
+ const config = requireProject();
1023
+ const normalized = mode.toLowerCase();
1024
+ if (!["on", "off"].includes(normalized)) throw new Error(`Expected "on" or "off" \u2014 got "${mode}"`);
1025
+ const target = normalized === "on";
1026
+ if (target) {
1027
+ await confirmOrDie(`Make every file in bucket "${name}" readable by anyone who knows its key (no login)?`, opts.yes);
1028
+ }
1029
+ const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}/storage/buckets/${encodeURIComponent(name)}`, {
1030
+ method: "PATCH",
1031
+ headers: { ...authHeaders2(config), "Content-Type": "application/json" },
1032
+ body: JSON.stringify({ public: target })
1033
+ });
1034
+ await throwOnError2(res);
1035
+ console.log(target ? `\u2713 Bucket "${name}" is now public` : `\u2713 Bucket "${name}" is now private`);
1036
+ }
1037
+ async function storageBucketsDeleteCommand(name, opts) {
1038
+ const config = requireProject();
1039
+ await confirmOrDie(`Remove bucket "${name}"? (files already stored under it are kept, only the access policy is removed)`, opts.yes);
1040
+ const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}/storage/buckets/${encodeURIComponent(name)}`, {
1041
+ method: "DELETE",
1042
+ headers: authHeaders2(config)
1043
+ });
1044
+ await throwOnError2(res);
1045
+ console.log(`\u2713 Removed bucket "${name}"`);
1046
+ }
1047
+ function parseDuration(input) {
1048
+ const match = /^(\d+)([smhd])?$/.exec(input.trim());
1049
+ if (!match) throw new Error(`Invalid duration "${input}" \u2014 expected e.g. "3600", "30m", "1h", "7d"`);
1050
+ const n = Number(match[1]);
1051
+ const unit = match[2] ?? "s";
1052
+ const mult = { s: 1, m: 60, h: 3600, d: 86400 }[unit] ?? 1;
1053
+ return n * mult;
1054
+ }
1055
+ async function storageSignCommand(key, opts) {
1056
+ const config = requireProject();
1057
+ const expiresIn = opts.expires ? parseDuration(opts.expires) : void 0;
1058
+ const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}/storage/signed-url`, {
1059
+ method: "POST",
1060
+ headers: { ...authHeaders2(config), "Content-Type": "application/json" },
1061
+ body: JSON.stringify({ key, expiresIn })
1062
+ });
1063
+ await throwOnError2(res);
1064
+ const data = await res.json();
1065
+ console.log(data.url);
1066
+ console.log(`(expires ${new Date(data.expiresAt).toLocaleString()})`);
1067
+ }
872
1068
 
873
1069
  // src/commands/webhooks.ts
874
1070
  var KNOWN_EVENTS = [
@@ -1512,6 +1708,51 @@ async function vpsStatusCommand() {
1512
1708
  );
1513
1709
  console.log("");
1514
1710
  }
1711
+ async function vpsRestartCommand(service) {
1712
+ const config = requireProject();
1713
+ const data = await apiRequest(`/api/projects/${config.projectId}/vps/restart`, {
1714
+ method: "POST",
1715
+ body: JSON.stringify({ service })
1716
+ });
1717
+ console.log(`\u2713 Restarted ${data.restarted}`);
1718
+ }
1719
+ async function vpsServiceMetricsCommand() {
1720
+ const config = requireProject();
1721
+ const data = await apiRequest(`/api/projects/${config.projectId}/vps/service-metrics`);
1722
+ if (!data.services.length) {
1723
+ console.log("(no services found \u2014 deploy first, or check that raw Postgres/Redis are provisioned)");
1724
+ return;
1725
+ }
1726
+ console.log("\n\u2500\u2500 SERVICE METRICS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1727
+ for (const s of data.services) {
1728
+ const restarts = s.restartCount ? ` \xB7 ${s.restartCount} restart${s.restartCount === 1 ? "" : "s"}` : "";
1729
+ const health = s.health ? ` \xB7 health: ${s.health}` : "";
1730
+ console.log(` ${s.state === "running" ? "\u25CF" : "\u25CB"} ${s.service} (${s.kind})`);
1731
+ console.log(` cpu: ${s.cpuPercent ?? "?"}% mem: ${s.memUsage ?? "?"} (${s.memPercent ?? "?"}%) net: ${s.netIO ?? "?"}${restarts}${health}`);
1732
+ }
1733
+ console.log("");
1734
+ }
1735
+ async function vpsWebMetricsCommand(opts) {
1736
+ const config = requireProject();
1737
+ const minutes = Math.min(Math.max(parseInt(opts.minutes ?? "15", 10) || 15, 1), 1440);
1738
+ const data = await apiRequest(`/api/projects/${config.projectId}/vps/web-metrics?minutes=${minutes}`);
1739
+ console.log(`
1740
+ \u2500\u2500 WEB REQUESTS (last ${data.windowMinutes}m) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
1741
+ if (data.note) {
1742
+ console.log(` ${data.note}`);
1743
+ } else if (data.requests === 0) {
1744
+ console.log(" No requests in this window.");
1745
+ } else {
1746
+ const statusLine = Object.entries(data.byStatusClass).sort(([a], [b]) => a.localeCompare(b)).map(([cls, n]) => `${cls}: ${n}`).join(" ");
1747
+ console.log(` ${data.requests} requests \xB7 avg ${data.avgResponseMs}ms \xB7 p95 ${data.p95ResponseMs}ms`);
1748
+ console.log(` ${statusLine}`);
1749
+ if (data.recentErrors?.length) {
1750
+ console.log("\n Recent 5xx:");
1751
+ for (const e of data.recentErrors) console.log(` ${e.time} ${e.status} ${e.method} ${e.uri} (${e.responseMs}ms)`);
1752
+ }
1753
+ }
1754
+ console.log("");
1755
+ }
1515
1756
  async function vpsLogsCommand(service, opts) {
1516
1757
  const config = requireProject();
1517
1758
  const tail = Math.min(Math.max(parseInt(opts.tail ?? "200", 10) || 200, 1), 5e3);
@@ -1531,6 +1772,7 @@ program.command("projects").description("List projects").action(projectsListComm
1531
1772
  program.command("create <name>").description("Create a new project (and set it active)").option("--slug <slug>", "Project slug (derived from name if omitted)").option("--repo <url>", "Public repo URL to link for deployments (no embedded tokens/credentials \u2014 for a private repo or auto-deploy on push, use 'deploro github connect' + 'deploro repo link' instead)").option("--branch <branch>", "Git branch to deploy from").action(projectsCreateCommand);
1532
1773
  program.command("use <project>").description("Set the active project (accepts id, slug, or name)").action(useProjectCommand);
1533
1774
  program.command("delete <project>").description("Delete a project and its database (accepts id, slug, or name)").option("-y, --yes", "Skip confirmation prompt").action(projectsDeleteCommand);
1775
+ 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);
1534
1776
  var db = program.command("db").description("Database commands");
1535
1777
  db.command("create").description("Provision the active project's database (idempotent)").action(dbCreateCommand);
1536
1778
  db.command("info").description("Show database status and how to reach it from a deployed app").action(dbInfoCommand);
@@ -1598,6 +1840,9 @@ vps.command("deployments").description("List VPS deploy history").action(vpsDepl
1598
1840
  vps.command("deploy-status [deployment-id]").description("Show a VPS deployment's status and logs (defaults to latest)").action(vpsDeployStatusCommand);
1599
1841
  vps.command("status").description("Show running compute services + raw Postgres/Redis container status").action(vpsStatusCommand);
1600
1842
  vps.command("logs [service]").description("Tail logs for a compute service (or 'postgres'/'redis')").option("--tail <n>", "Number of lines", "200").action(vpsLogsCommand);
1843
+ vps.command("restart <service>").description("Restart one compute service (or 'postgres'/'redis') without a full redeploy").action(vpsRestartCommand);
1844
+ vps.command("service-metrics").description("Per-container CPU/memory/health/restart count for every compute service").action(vpsServiceMetricsCommand);
1845
+ vps.command("web-metrics").description("Request count, status breakdown, and response-time stats for the public web service").option("--minutes <n>", "Time window", "15").action(vpsWebMetricsCommand);
1601
1846
  var github = program.command("github").description("GitHub account connection (OAuth)");
1602
1847
  github.command("status").description("Show whether your GitHub account is connected").action(githubStatusCommand);
1603
1848
  github.command("connect").description("Print the URL to open in a browser to connect GitHub").action(githubConnectCommand);
@@ -1616,9 +1861,16 @@ domains.command("status <id-or-hostname>").description("Poll domain verification
1616
1861
  domains.command("remove <id-or-hostname>").description("Remove a domain").option("-y, --yes", "Skip confirmation prompt").action(domainsRemoveCommand);
1617
1862
  var storage = program.command("storage").description("R2 file storage for the active project");
1618
1863
  storage.command("list").description("List stored files").action(storageListCommand);
1619
- storage.command("upload <file>").description("Upload a local file").option("--key <name>", "Stored file name (defaults to the local file's name)").action(storageUploadCommand);
1864
+ 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);
1620
1865
  storage.command("download <key>").description("Download a stored file").option("--out <path>", "Local path to save to (defaults to the file's name)").action(storageDownloadCommand);
1621
1866
  storage.command("delete <key>").description("Delete a stored file").option("-y, --yes", "Skip confirmation prompt").action(storageDeleteCommand);
1867
+ storage.command("public <mode>").description("Turn unauthenticated read access on|off, or show status").option("-y, --yes", "Skip confirmation prompt").action(storagePublicCommand);
1868
+ storage.command("sign <key>").description("Generate a time-limited public URL for one file").option("--expires <duration>", "e.g. 1h, 30m, 7d (default 1h)").action(storageSignCommand);
1869
+ var buckets = storage.command("buckets").description("Named, independently-public storage groupings within this project");
1870
+ buckets.command("list").description("List buckets").action(storageBucketsListCommand);
1871
+ buckets.command("create <name>").description("Create a bucket").option("--public", "Make it public read on creation").action(storageBucketsCreateCommand);
1872
+ buckets.command("set-public <name> <mode>").description("Turn a bucket's unauthenticated read access on|off").option("-y, --yes", "Skip confirmation prompt").action(storageBucketsSetPublicCommand);
1873
+ buckets.command("delete <name>").description("Remove a bucket's access policy (keeps its files)").option("-y, --yes", "Skip confirmation prompt").action(storageBucketsDeleteCommand);
1622
1874
  var webhooks = program.command("webhooks").description("Outbound webhooks for the active project");
1623
1875
  webhooks.command("list").description("List webhooks").action(webhooksListCommand);
1624
1876
  webhooks.command("create <url>").description("Create a webhook").option("--events <list>", "Comma-separated event names (default: test)").option("--secret <secret>", "HMAC signing secret").action(webhooksCreateCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploro",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Command-line interface for the Deploro developer platform — manage projects, databases, deployments, and hosting from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {