deploro 0.3.4 → 0.5.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 +643 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -379,6 +379,24 @@ async function authUsersCommand() {
379
379
  }))
380
380
  );
381
381
  }
382
+ async function authBanCommand(id, opts) {
383
+ requireConfig();
384
+ await confirmOrDie(`Ban user ${id}?`, opts.yes);
385
+ const expiresAt = opts.days ? new Date(Date.now() + Number(opts.days) * 24 * 60 * 60 * 1e3).toISOString() : void 0;
386
+ await apiRequest(`/api/auth/users/${encodeURIComponent(id)}/ban`, {
387
+ method: "PATCH",
388
+ body: JSON.stringify({ banned: true, reason: opts.reason, expiresAt })
389
+ });
390
+ console.log(`\u2713 User ${id} banned${opts.days ? ` for ${opts.days} day(s)` : ""}`);
391
+ }
392
+ async function authUnbanCommand(id) {
393
+ requireConfig();
394
+ await apiRequest(`/api/auth/users/${encodeURIComponent(id)}/ban`, {
395
+ method: "PATCH",
396
+ body: JSON.stringify({ banned: false })
397
+ });
398
+ console.log(`\u2713 User ${id} unbanned`);
399
+ }
382
400
  async function authInviteCommand(email, opts) {
383
401
  requireConfig();
384
402
  const role = opts.admin ? "admin" : "member";
@@ -518,7 +536,8 @@ async function authProvidersListCommand() {
518
536
  "reddit",
519
537
  "dropbox",
520
538
  "notion",
521
- "apple"
539
+ "apple",
540
+ "upwork"
522
541
  ];
523
542
  const byProvider = new Map(data.providers.map((p) => [p.provider, p]));
524
543
  const rows = known.map((provider) => {
@@ -539,6 +558,11 @@ async function authProviderEnableCommand(provider, enabled) {
539
558
  { method: "PUT", body: JSON.stringify({ enabled }) }
540
559
  );
541
560
  console.log(`\u2713 ${provider} ${enabled ? "enabled" : "disabled"}`);
561
+ if (provider === "email_password" && enabled) {
562
+ console.log(
563
+ " Your app can now call /auth/:slug/email-password/{signup,login,request-reset,reset} directly \u2014\n see the dashboard's Auth page for the exact copyable URLs."
564
+ );
565
+ }
542
566
  }
543
567
  async function authProviderConfigureCommand(provider, opts) {
544
568
  const config = requireProject();
@@ -616,12 +640,355 @@ async function authStatsCommand() {
616
640
  }
617
641
  }
618
642
 
643
+ // src/commands/github.ts
644
+ async function githubStatusCommand() {
645
+ requireConfig();
646
+ const data = await apiRequestNoProject("/api/github/status");
647
+ if (!data.configured) {
648
+ console.log("GitHub OAuth is not configured on this platform (operator hasn't set GITHUB_APP_CLIENT_ID/SECRET).");
649
+ return;
650
+ }
651
+ console.log(data.connected ? `\u2713 Connected as ${data.login}` : "Not connected. Run: deploro github connect");
652
+ }
653
+ async function githubConnectCommand() {
654
+ requireConfig();
655
+ const data = await apiRequestNoProject("/api/github/oauth/start");
656
+ console.log("Open this URL in a browser to connect your GitHub account:\n");
657
+ console.log(` ${data.url}
658
+ `);
659
+ console.log("After approving, run 'deploro github status' to confirm.");
660
+ }
661
+ async function githubReposCommand() {
662
+ requireConfig();
663
+ const data = await apiRequestNoProject(
664
+ "/api/github/repos"
665
+ );
666
+ if (!data.configured) {
667
+ console.log("GitHub OAuth is not configured on this platform.");
668
+ return;
669
+ }
670
+ if (!data.connected) {
671
+ console.log("Not connected. Run: deploro github connect");
672
+ return;
673
+ }
674
+ if (data.error) {
675
+ console.error(data.error);
676
+ return;
677
+ }
678
+ table(
679
+ data.repos.map((r) => ({
680
+ repo: r.full_name,
681
+ branch: r.default_branch,
682
+ visibility: r.private ? "private" : "public"
683
+ }))
684
+ );
685
+ console.log("\nLink one to the active project with: deploro repo link <owner/repo>");
686
+ }
687
+ async function githubDisconnectCommand(opts) {
688
+ requireConfig();
689
+ await confirmOrDie(
690
+ "Disconnect your GitHub account? Projects with a repo already linked keep deploying \u2014 this only removes your stored OAuth token, so you won't be able to link new repos until you reconnect.",
691
+ opts.yes
692
+ );
693
+ await apiRequestNoProject("/api/github/disconnect", { method: "POST" });
694
+ console.log("\u2713 GitHub account disconnected");
695
+ }
696
+ async function repoStatusCommand() {
697
+ const config = requireProject();
698
+ const data = await apiRequest(
699
+ `/api/projects/${config.projectId}/github`
700
+ );
701
+ if (!data.connection) {
702
+ console.log("No repo linked. Run: deploro repo link <owner/repo>");
703
+ return;
704
+ }
705
+ console.log(`Repo : ${data.connection.github_owner}/${data.connection.github_repo}`);
706
+ console.log(`Branch : ${data.connection.branch}`);
707
+ console.log(`Webhook: ${data.connection.webhook_id ? "auto-created on GitHub" : "(not created \u2014 deploys may not trigger)"}`);
708
+ }
709
+ async function repoLinkCommand(ownerRepo, opts) {
710
+ const config = requireProject();
711
+ const [owner, repo2] = ownerRepo.split("/");
712
+ if (!owner || !repo2) {
713
+ console.error('Expected "owner/repo", e.g. "acme/my-app"');
714
+ process.exit(1);
715
+ }
716
+ const data = await apiRequest(
717
+ `/api/projects/${config.projectId}/github`,
718
+ { method: "POST", body: JSON.stringify({ owner, repo: repo2, branch: opts.branch }) }
719
+ );
720
+ console.log(`\u2713 Linked ${data.connection.owner}/${data.connection.repo} (branch: ${data.connection.branch})`);
721
+ console.log(
722
+ data.connection.webhook_created ? " Webhook created on GitHub \u2014 pushes will now auto-deploy." : " Webhook already existed on GitHub \u2014 reusing it."
723
+ );
724
+ }
725
+ async function repoUnlinkCommand(opts) {
726
+ const config = requireProject();
727
+ await confirmOrDie("Unlink this project's repo and remove its GitHub webhook?", opts.yes);
728
+ await apiRequest(`/api/projects/${config.projectId}/github`, { method: "DELETE" });
729
+ console.log("\u2713 Repo unlinked");
730
+ }
731
+ async function repoAutoDeployCommand(state) {
732
+ const config = requireProject();
733
+ const enabled = state === "on" || state === "true" || state === "enable";
734
+ const disabled = state === "off" || state === "false" || state === "disable";
735
+ if (!enabled && !disabled) {
736
+ console.error('Expected "on" or "off"');
737
+ process.exit(1);
738
+ }
739
+ await apiRequest(`/api/projects/${config.projectId}`, {
740
+ method: "PATCH",
741
+ body: JSON.stringify({ auto_deploy_enabled: enabled })
742
+ });
743
+ console.log(`\u2713 Automatic deployment ${enabled ? "enabled" : "disabled"}`);
744
+ }
745
+
746
+ // src/commands/domains.ts
747
+ async function domainsListCommand() {
748
+ const config = requireProject();
749
+ const data = await apiRequest(`/api/projects/${config.projectId}/domains`);
750
+ table(
751
+ data.domains.map((d) => ({
752
+ hostname: d.hostname,
753
+ status: d.status,
754
+ ssl: d.ssl_status,
755
+ verified: d.verified_at ? "yes" : "no"
756
+ }))
757
+ );
758
+ }
759
+ async function domainsAddCommand(hostname) {
760
+ const config = requireProject();
761
+ const data = await apiRequest(`/api/projects/${config.projectId}/domains`, {
762
+ method: "POST",
763
+ body: JSON.stringify({ hostname })
764
+ });
765
+ console.log(`\u2713 Domain added: ${data.domain.hostname} (id: ${data.domain.id})
766
+ `);
767
+ console.log("Add these DNS records at your registrar/DNS provider, then run 'deploro domains verify " + data.domain.id + "':\n");
768
+ const { cname, txt } = data.dns_instructions;
769
+ console.log(` ${cname.type} ${cname.name} \u2192 ${cname.value}`);
770
+ console.log(` ${txt.type} ${txt.name} \u2192 ${txt.value}`);
771
+ }
772
+ async function domainsVerifyCommand(idOrHostname) {
773
+ const config = requireProject();
774
+ const id = await resolveDomainId(config.projectId, idOrHostname);
775
+ const data = await apiRequest(
776
+ `/api/projects/${config.projectId}/domains/${id}/verify`,
777
+ { method: "POST" }
778
+ );
779
+ console.log(data.verified ? `\u2713 ${data.message}` : `\u2717 ${data.message}`);
780
+ }
781
+ async function domainsStatusCommand(idOrHostname) {
782
+ const config = requireProject();
783
+ const id = await resolveDomainId(config.projectId, idOrHostname);
784
+ const data = await apiRequest(
785
+ `/api/projects/${config.projectId}/domains/${id}/status`
786
+ );
787
+ console.log(`Status: ${data.status}`);
788
+ console.log(`SSL : ${data.ssl_status}`);
789
+ }
790
+ async function domainsRemoveCommand(idOrHostname, opts) {
791
+ const config = requireProject();
792
+ const id = await resolveDomainId(config.projectId, idOrHostname);
793
+ await confirmOrDie(`Remove domain "${idOrHostname}"? Traffic to it will stop resolving.`, opts.yes);
794
+ await apiRequest(`/api/projects/${config.projectId}/domains/${id}`, { method: "DELETE" });
795
+ console.log("\u2713 Domain removed");
796
+ }
797
+ async function resolveDomainId(projectId, idOrHostname) {
798
+ const data = await apiRequest(`/api/projects/${projectId}/domains`);
799
+ const match = data.domains.find((d) => d.id === idOrHostname || d.hostname === idOrHostname.toLowerCase());
800
+ if (!match) {
801
+ console.error(`No domain matches "${idOrHostname}". Run: deploro domains list`);
802
+ process.exit(1);
803
+ }
804
+ return match.id;
805
+ }
806
+
807
+ // src/commands/storage.ts
808
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
809
+ import { basename } from "path";
810
+ function authHeaders(config) {
811
+ return { Authorization: `Bearer ${config.accessToken}` };
812
+ }
813
+ async function throwOnError(res) {
814
+ if (res.ok) return;
815
+ const body = await res.json().catch(() => ({ error: res.statusText }));
816
+ throw new Error(`${res.status} ${body.error ?? res.statusText}`);
817
+ }
818
+ async function storageListCommand() {
819
+ const config = requireProject();
820
+ const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}/storage`, {
821
+ headers: authHeaders(config)
822
+ });
823
+ await throwOnError(res);
824
+ const data = await res.json();
825
+ table(
826
+ data.objects.map((o) => ({
827
+ key: o.key,
828
+ size: `${(o.size / 1024).toFixed(1)} KB`,
829
+ type: o.contentType,
830
+ uploaded: new Date(o.uploaded).toLocaleString()
831
+ }))
832
+ );
833
+ if (data.truncated) console.log("\n(more objects exist \u2014 pagination isn't wired up here yet, use the dashboard for the full list)");
834
+ }
835
+ async function storageUploadCommand(filePath, opts) {
836
+ const config = requireProject();
837
+ const bytes = readFileSync2(filePath);
838
+ const filename = opts.key ?? basename(filePath);
839
+ const form = new FormData();
840
+ form.append("file", new Blob([bytes]), filename);
841
+ const res = await fetch(`${config.baseUrl}/api/projects/${config.projectId}/storage/upload`, {
842
+ method: "POST",
843
+ headers: authHeaders(config),
844
+ body: form
845
+ });
846
+ await throwOnError(res);
847
+ const data = await res.json();
848
+ console.log(`\u2713 Uploaded as ${data.key} (${data.size} bytes, ${data.contentType})`);
849
+ }
850
+ async function storageDownloadCommand(key, opts) {
851
+ const config = requireProject();
852
+ const res = await fetch(
853
+ `${config.baseUrl}/api/projects/${config.projectId}/storage/download?key=${encodeURIComponent(key)}`,
854
+ { headers: authHeaders(config) }
855
+ );
856
+ await throwOnError(res);
857
+ const bytes = new Uint8Array(await res.arrayBuffer());
858
+ const outPath = opts.out ?? basename(key);
859
+ writeFileSync2(outPath, bytes);
860
+ console.log(`\u2713 Saved to ${outPath} (${bytes.length} bytes)`);
861
+ }
862
+ async function storageDeleteCommand(key, opts) {
863
+ const config = requireProject();
864
+ await confirmOrDie(`Delete "${key}" from storage?`, opts.yes);
865
+ const res = await fetch(
866
+ `${config.baseUrl}/api/projects/${config.projectId}/storage?key=${encodeURIComponent(key)}`,
867
+ { method: "DELETE", headers: authHeaders(config) }
868
+ );
869
+ await throwOnError(res);
870
+ console.log(`\u2713 Deleted ${key}`);
871
+ }
872
+
873
+ // src/commands/webhooks.ts
874
+ var KNOWN_EVENTS = [
875
+ "db.row.insert",
876
+ "db.row.update",
877
+ "db.row.delete",
878
+ "deployment.started",
879
+ "deployment.success",
880
+ "deployment.failed",
881
+ "test"
882
+ ];
883
+ async function webhooksListCommand() {
884
+ const config = requireProject();
885
+ const data = await apiRequest(`/api/projects/${config.projectId}/webhooks`);
886
+ table(
887
+ data.webhooks.map((w) => ({
888
+ id: w.id,
889
+ url: w.url,
890
+ events: w.events.join(","),
891
+ enabled: w.enabled,
892
+ deliveries: w.delivery_count,
893
+ lastStatus: w.last_status_code ?? "-"
894
+ }))
895
+ );
896
+ }
897
+ async function webhooksCreateCommand(url, opts) {
898
+ const config = requireProject();
899
+ const events = opts.events ? opts.events.split(",").map((e) => e.trim()).filter(Boolean) : ["test"];
900
+ const data = await apiRequest(`/api/projects/${config.projectId}/webhooks`, {
901
+ method: "POST",
902
+ body: JSON.stringify({ url, events, secret: opts.secret })
903
+ });
904
+ console.log(`\u2713 Webhook created: ${data.webhook.id}`);
905
+ console.log(` Events: ${data.webhook.events.join(", ")}`);
906
+ if (!opts.events) {
907
+ console.log(` (no --events passed \u2014 defaulted to "test". Known events: ${KNOWN_EVENTS.join(", ")})`);
908
+ }
909
+ }
910
+ async function webhooksUpdateCommand(id, opts) {
911
+ const config = requireProject();
912
+ const body = {};
913
+ if (opts.url) body.url = opts.url;
914
+ if (opts.events) body.events = opts.events.split(",").map((e) => e.trim()).filter(Boolean);
915
+ if (opts.secret) body.secret = opts.secret;
916
+ if (opts.enable) body.enabled = true;
917
+ if (opts.disable) body.enabled = false;
918
+ if (Object.keys(body).length === 0) {
919
+ console.error("Nothing to update \u2014 pass --url, --events, --secret, --enable, or --disable");
920
+ process.exit(1);
921
+ }
922
+ await apiRequest(`/api/projects/${config.projectId}/webhooks/${id}`, {
923
+ method: "PATCH",
924
+ body: JSON.stringify(body)
925
+ });
926
+ console.log(`\u2713 Webhook ${id} updated`);
927
+ }
928
+ async function webhooksDeleteCommand(id, opts) {
929
+ const config = requireProject();
930
+ await confirmOrDie(`Delete webhook ${id}?`, opts.yes);
931
+ await apiRequest(`/api/projects/${config.projectId}/webhooks/${id}`, { method: "DELETE" });
932
+ console.log(`\u2713 Webhook ${id} deleted`);
933
+ }
934
+ async function webhooksTestCommand(id) {
935
+ const config = requireProject();
936
+ await apiRequest(`/api/projects/${config.projectId}/webhooks/${id}/test`, { method: "POST" });
937
+ console.log("\u2713 Test payload dispatched \u2014 check 'deploro webhooks deliveries' for the result.");
938
+ }
939
+ async function webhooksDeliveriesCommand(id) {
940
+ const config = requireProject();
941
+ const data = await apiRequest(
942
+ `/api/projects/${config.projectId}/webhooks/${id}/deliveries`
943
+ );
944
+ table(
945
+ data.deliveries.map((d) => ({
946
+ event: d.event,
947
+ status: d.status_code ?? "-",
948
+ ms: d.duration_ms ?? "-",
949
+ error: d.error ?? "",
950
+ at: new Date(d.delivered_at).toLocaleString()
951
+ }))
952
+ );
953
+ }
954
+
955
+ // src/commands/team.ts
956
+ async function teamListCommand() {
957
+ const config = requireProject();
958
+ const data = await apiRequest(`/api/projects/${config.projectId}/members`);
959
+ table(
960
+ data.members.map((m) => ({
961
+ id: m.id,
962
+ name: m.name,
963
+ email: m.email,
964
+ role: m.role,
965
+ joined: new Date(m.joined_at).toLocaleDateString()
966
+ }))
967
+ );
968
+ }
969
+ async function teamInviteCommand(email, opts) {
970
+ const config = requireProject();
971
+ const role = opts.admin ? "admin" : "member";
972
+ await apiRequest(`/api/projects/${config.projectId}/members`, {
973
+ method: "POST",
974
+ body: JSON.stringify({ email, role })
975
+ });
976
+ console.log(`\u2713 Added ${email} to the project as ${role}`);
977
+ console.log(" (they must already have a Deploro account \u2014 use 'deploro auth invite' first if not)");
978
+ }
979
+ async function teamRemoveCommand(memberId, opts) {
980
+ const config = requireProject();
981
+ await confirmOrDie(`Remove member ${memberId} from this project?`, opts.yes);
982
+ await apiRequest(`/api/projects/${config.projectId}/members/${memberId}`, { method: "DELETE" });
983
+ console.log(`\u2713 Member ${memberId} removed`);
984
+ }
985
+
619
986
  // src/commands/migrate.ts
620
- import { readFileSync as readFileSync2 } from "fs";
987
+ import { readFileSync as readFileSync3 } from "fs";
621
988
  function readSqlOpt(inline, file, label) {
622
989
  if (file) {
623
990
  try {
624
- return readFileSync2(file, "utf-8");
991
+ return readFileSync3(file, "utf-8");
625
992
  } catch (err) {
626
993
  console.error(`Could not read --${label}-file "${file}": ${err instanceof Error ? err.message : String(err)}`);
627
994
  process.exit(1);
@@ -716,9 +1083,7 @@ async function migrateDeleteCommand(idOrName, opts) {
716
1083
  // src/commands/hosting.ts
717
1084
  async function hostingStatusCommand() {
718
1085
  requireConfig();
719
- const data = await apiRequest(
720
- "/api/hosting/status"
721
- );
1086
+ const data = await apiRequest("/api/hosting/status");
722
1087
  console.log("\n\u2500\u2500 WORKER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
723
1088
  if (data.worker) {
724
1089
  console.log(` Script : ${data.worker.name}`);
@@ -748,6 +1113,10 @@ async function hostingStatusCommand() {
748
1113
  } else {
749
1114
  console.log(" (no VPS info)");
750
1115
  }
1116
+ if (data.untrackedContainers && data.untrackedContainers.length > 0) {
1117
+ console.log("\n \u26A0 Untracked containers (didn't come from a Deployer-issued deploy):");
1118
+ for (const name of data.untrackedContainers) console.log(` - ${name}`);
1119
+ }
751
1120
  console.log("");
752
1121
  }
753
1122
  async function hostingSetEnvCommand(key, value) {
@@ -893,7 +1262,7 @@ async function deployCommand() {
893
1262
  async function deployStatusCommand() {
894
1263
  const config = requireProject();
895
1264
  const data = await apiRequest(
896
- `/api/projects/${config.projectId}/deployments`
1265
+ `/api/projects/${config.projectId}/deployments/all`
897
1266
  );
898
1267
  const latest = data.deployments[0];
899
1268
  if (!latest) {
@@ -903,6 +1272,7 @@ async function deployStatusCommand() {
903
1272
  console.log(`
904
1273
  \u2500\u2500 LATEST DEPLOYMENT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
905
1274
  console.log(` ID : ${latest.id}`);
1275
+ console.log(` Target : ${latest.target ?? "worker"}`);
906
1276
  console.log(` Status : ${latest.status}`);
907
1277
  console.log(` Branch : ${latest.git_branch ?? "(none)"}`);
908
1278
  console.log(` Started : ${new Date(latest.created_at).toLocaleString()}`);
@@ -914,7 +1284,7 @@ async function deployLogsCommand(deploymentId) {
914
1284
  let id = deploymentId;
915
1285
  if (!id) {
916
1286
  const list = await apiRequest(
917
- `/api/projects/${config.projectId}/deployments`
1287
+ `/api/projects/${config.projectId}/deployments/all`
918
1288
  );
919
1289
  id = list.deployments[0]?.id;
920
1290
  if (!id) {
@@ -928,6 +1298,7 @@ async function deployLogsCommand(deploymentId) {
928
1298
  const d = data.deployment;
929
1299
  console.log(`
930
1300
  \u2500\u2500 DEPLOYMENT ${d.id} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
1301
+ console.log(` Target : ${d.target ?? "worker"}`);
931
1302
  console.log(` Status : ${d.status}`);
932
1303
  console.log(
933
1304
  ` Commit : ${d.git_branch ?? "(none)"}${d.git_sha ? ` @ ${String(d.git_sha).slice(0, 7)}` : ""}`
@@ -941,6 +1312,215 @@ async function deployLogsCommand(deploymentId) {
941
1312
  console.log("");
942
1313
  }
943
1314
 
1315
+ // src/commands/vps.ts
1316
+ async function vpsDatabaseCreateCommand() {
1317
+ const config = requireProject();
1318
+ const data = await apiRequest(`/api/projects/${config.projectId}/vps/database`, { method: "POST" });
1319
+ if (data.already_provisioned) {
1320
+ console.log("Already provisioned. The connection string is only ever shown once (on creation, or after a rotate).");
1321
+ console.log(` Host : ${data.host ?? "(unknown \u2014 VPS_PUBLIC_HOST not configured on the platform)"}`);
1322
+ console.log(` Port : ${data.port}`);
1323
+ console.log(` User : ${data.user}`);
1324
+ console.log(` DB : ${data.database}`);
1325
+ return;
1326
+ }
1327
+ console.log("\u2713 Raw Postgres provisioned:");
1328
+ console.log(` ${data.database_url}`);
1329
+ console.log(`
1330
+ ${data.note ?? ""}`);
1331
+ }
1332
+ async function vpsDatabaseInfoCommand() {
1333
+ const config = requireProject();
1334
+ const data = await apiRequest(
1335
+ `/api/projects/${config.projectId}/vps/database`
1336
+ );
1337
+ if (!data.provisioned) {
1338
+ console.log("Not provisioned. Run: deploro vps database create");
1339
+ return;
1340
+ }
1341
+ console.log("\n\u2500\u2500 RAW POSTGRES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1342
+ console.log(` Host : ${data.host ?? "(unknown \u2014 VPS_PUBLIC_HOST not configured on the platform)"}`);
1343
+ console.log(` Port : ${data.port}`);
1344
+ console.log(` User : ${data.user}`);
1345
+ console.log(` DB : ${data.database}`);
1346
+ console.log(" (password not stored in plaintext \u2014 run 'deploro vps database rotate' to issue a new one)\n");
1347
+ }
1348
+ async function vpsDatabaseRotateCommand(opts) {
1349
+ const config = requireProject();
1350
+ await confirmOrDie("Rotate the raw Postgres password? Anything using the old connection string will stop working until redeployed.", opts.yes);
1351
+ const data = await apiRequest(
1352
+ `/api/projects/${config.projectId}/vps/database/rotate`,
1353
+ { method: "POST" }
1354
+ );
1355
+ console.log("\u2713 Password rotated:");
1356
+ console.log(` ${data.database_url}`);
1357
+ console.log(`
1358
+ ${data.note}`);
1359
+ }
1360
+ async function vpsRedisCreateCommand() {
1361
+ const config = requireProject();
1362
+ const data = await apiRequest(`/api/projects/${config.projectId}/vps/redis`, { method: "POST" });
1363
+ if (data.already_provisioned) {
1364
+ console.log("Already provisioned. The connection string is only ever shown once (on creation, or after a rotate).");
1365
+ console.log(` Host : ${data.host ?? "(unknown \u2014 VPS_PUBLIC_HOST not configured on the platform)"}`);
1366
+ console.log(` Port : ${data.port}`);
1367
+ return;
1368
+ }
1369
+ console.log("\u2713 Raw Redis provisioned:");
1370
+ console.log(` ${data.redis_url}`);
1371
+ console.log(`
1372
+ ${data.note ?? ""}`);
1373
+ }
1374
+ async function vpsRedisInfoCommand() {
1375
+ const config = requireProject();
1376
+ const data = await apiRequest(
1377
+ `/api/projects/${config.projectId}/vps/redis`
1378
+ );
1379
+ if (!data.provisioned) {
1380
+ console.log("Not provisioned. Run: deploro vps redis create");
1381
+ return;
1382
+ }
1383
+ console.log("\n\u2500\u2500 RAW REDIS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1384
+ console.log(` Host : ${data.host ?? "(unknown \u2014 VPS_PUBLIC_HOST not configured on the platform)"}`);
1385
+ console.log(` Port : ${data.port}`);
1386
+ console.log(" (password not stored in plaintext \u2014 run 'deploro vps redis rotate' to issue a new one)\n");
1387
+ }
1388
+ async function vpsRedisRotateCommand(opts) {
1389
+ const config = requireProject();
1390
+ await confirmOrDie("Rotate the raw Redis password? This briefly restarts the Redis container. Anything using the old connection string will stop working until redeployed.", opts.yes);
1391
+ const data = await apiRequest(
1392
+ `/api/projects/${config.projectId}/vps/redis/rotate`,
1393
+ { method: "POST" }
1394
+ );
1395
+ console.log("\u2713 Password rotated:");
1396
+ console.log(` ${data.redis_url}`);
1397
+ console.log(`
1398
+ ${data.note}`);
1399
+ }
1400
+ async function vpsAllowlistListCommand() {
1401
+ const config = requireProject();
1402
+ const data = await apiRequest(
1403
+ `/api/projects/${config.projectId}/vps/allowlist`
1404
+ );
1405
+ if (data.entries.length === 0) {
1406
+ console.log("No allowlist entries \u2014 raw Postgres/Redis ports accept no inbound connections until you add one.");
1407
+ return;
1408
+ }
1409
+ table(data.entries.map((e) => ({ id: e.id, cidr: e.cidr, label: e.label ?? "", added: new Date(e.created_at).toLocaleString() })));
1410
+ }
1411
+ async function vpsAllowlistAddCommand(cidr, opts) {
1412
+ const config = requireProject();
1413
+ await apiRequest(`/api/projects/${config.projectId}/vps/allowlist`, {
1414
+ method: "POST",
1415
+ body: JSON.stringify({ cidr, label: opts.label })
1416
+ });
1417
+ console.log(`\u2713 Allowed ${cidr}${opts.label ? ` (${opts.label})` : ""} to reach this project's raw Postgres/Redis ports`);
1418
+ }
1419
+ async function vpsAllowlistRemoveCommand(id, opts) {
1420
+ const config = requireProject();
1421
+ await confirmOrDie(`Remove allowlist entry ${id}?`, opts.yes);
1422
+ await apiRequest(`/api/projects/${config.projectId}/vps/allowlist/${encodeURIComponent(id)}`, { method: "DELETE" });
1423
+ console.log(`\u2713 Allowlist entry ${id} removed`);
1424
+ }
1425
+ async function vpsEnvCommand() {
1426
+ const config = requireProject();
1427
+ const data = await apiRequest(`/api/projects/${config.projectId}/vps/env`);
1428
+ if (data.vars.length === 0) {
1429
+ console.log("No VPS env vars set on the active project.");
1430
+ return;
1431
+ }
1432
+ console.log("\nVPS-scoped env vars (written into the compute stack's .env; values are never returned by the API):\n");
1433
+ for (const v of data.vars) console.log(` ${v.key}`);
1434
+ console.log("");
1435
+ }
1436
+ async function vpsSetEnvCommand(key, value) {
1437
+ const config = requireProject();
1438
+ await apiRequest(`/api/projects/${config.projectId}/vps/env`, {
1439
+ method: "POST",
1440
+ body: JSON.stringify({ key, value })
1441
+ });
1442
+ console.log(`\u2713 VPS env var '${key}' set \u2014 run 'deploro vps deploy' to apply it`);
1443
+ }
1444
+ async function vpsDeployCommand() {
1445
+ const config = requireProject();
1446
+ const data = await apiRequest(
1447
+ `/api/projects/${config.projectId}/vps/deploy`,
1448
+ { method: "POST" }
1449
+ );
1450
+ console.log(`\u2713 VPS deploy triggered: ${data.deployment.id}`);
1451
+ console.log(" Run 'deploro vps deployments' to see history, or 'deploro vps logs' once containers are up.");
1452
+ }
1453
+ async function vpsDeploymentsCommand() {
1454
+ const config = requireProject();
1455
+ const data = await apiRequest(`/api/projects/${config.projectId}/vps/deployments`);
1456
+ if (data.deployments.length === 0) {
1457
+ console.log("No VPS deployments yet. Run: deploro vps deploy");
1458
+ return;
1459
+ }
1460
+ table(data.deployments.map((d) => ({
1461
+ id: d.id,
1462
+ status: d.status,
1463
+ branch: d.git_branch ?? "",
1464
+ started: new Date(d.created_at).toLocaleString(),
1465
+ finished: d.finished_at ? new Date(d.finished_at).toLocaleString() : "(in progress)"
1466
+ })));
1467
+ }
1468
+ async function vpsDeployStatusCommand(deploymentId) {
1469
+ const config = requireProject();
1470
+ let id = deploymentId;
1471
+ if (!id) {
1472
+ const list = await apiRequest(`/api/projects/${config.projectId}/vps/deployments`);
1473
+ id = list.deployments[0]?.id;
1474
+ if (!id) {
1475
+ console.log("No VPS deployments yet.");
1476
+ return;
1477
+ }
1478
+ }
1479
+ const data = await apiRequest(`/api/projects/${config.projectId}/vps/deployments/${id}`);
1480
+ const d = data.deployment;
1481
+ console.log(`
1482
+ \u2500\u2500 VPS DEPLOYMENT ${d.id} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
1483
+ console.log(` Status : ${d.status}`);
1484
+ console.log(` Branch : ${d.git_branch ?? "(none)"}`);
1485
+ if (d.agent_error) console.log(` Agent error: ${d.agent_error}`);
1486
+ console.log(`
1487
+ \u2500\u2500 LOGS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
1488
+ console.log(typeof d.logs === "string" && d.logs.trim().length > 0 ? d.logs : "(no logs)");
1489
+ console.log("");
1490
+ }
1491
+ async function vpsStatusCommand() {
1492
+ const config = requireProject();
1493
+ const data = await apiRequest(`/api/projects/${config.projectId}/vps/status`);
1494
+ console.log("\n\u2500\u2500 COMPUTE SERVICES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1495
+ if (data.servicesError) {
1496
+ console.log(` ${data.servicesError}`);
1497
+ } else if (data.services.length === 0) {
1498
+ console.log(" (none deployed \u2014 run: deploro vps deploy)");
1499
+ } else {
1500
+ for (const s of data.services) {
1501
+ const running = (s.State ?? "").toLowerCase().includes("running");
1502
+ console.log(` ${running ? "\u25CF" : "\u25CB"} ${s.Service ?? "?"} ${s.Status ?? s.State ?? ""}`);
1503
+ }
1504
+ }
1505
+ console.log("\n\u2500\u2500 RAW POSTGRES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1506
+ console.log(data.postgres ? ` ${data.postgres.running ? "\u25CF" : "\u25CB"} ${data.postgres.status}` : " (not provisioned)");
1507
+ console.log("\n\u2500\u2500 RAW REDIS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1508
+ console.log(data.redis ? ` ${data.redis.running ? "\u25CF" : "\u25CB"} ${data.redis.status}` : " (not provisioned)");
1509
+ console.log("\n\u2500\u2500 WEB \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1510
+ console.log(
1511
+ data.web_url ? ` ${data.web_url} (nginx proxy on the VPS, TLS via Let's Encrypt)` : " (none detected \u2014 compose must publish an HTTP port, then redeploy)"
1512
+ );
1513
+ console.log("");
1514
+ }
1515
+ async function vpsLogsCommand(service, opts) {
1516
+ const config = requireProject();
1517
+ const tail = Math.min(Math.max(parseInt(opts.tail ?? "200", 10) || 200, 1), 5e3);
1518
+ const qs = new URLSearchParams({ tail: String(tail) });
1519
+ if (service) qs.set("service", service);
1520
+ const data = await apiRequest(`/api/projects/${config.projectId}/vps/logs?${qs.toString()}`);
1521
+ console.log(data.logs.trim().length > 0 ? data.logs : "(no logs)");
1522
+ }
1523
+
944
1524
  // src/index.ts
945
1525
  var pkg = createRequire(import.meta.url)("../package.json");
946
1526
  var program = new Command();
@@ -948,7 +1528,7 @@ program.name("deploro").description("Deploro developer platform CLI").version(pk
948
1528
  program.command("login").description("Log in to Deploro").option("--url <url>", "Worker base URL").option("--token <token>", "Personal access token from the Deploro dashboard").action(loginCommand);
949
1529
  program.command("logout").description("Log out and clear local credentials").action(logoutCommand);
950
1530
  program.command("projects").description("List projects").action(projectsListCommand);
951
- 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>", "GitHub repo URL to link for deployments").option("--branch <branch>", "Git branch to deploy from").action(projectsCreateCommand);
1531
+ 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);
952
1532
  program.command("use <project>").description("Set the active project (accepts id, slug, or name)").action(useProjectCommand);
953
1533
  program.command("delete <project>").description("Delete a project and its database (accepts id, slug, or name)").option("-y, --yes", "Skip confirmation prompt").action(projectsDeleteCommand);
954
1534
  var db = program.command("db").description("Database commands");
@@ -972,6 +1552,8 @@ var auth = program.command("auth").description("Auth management commands");
972
1552
  auth.command("whoami").description("Show the current identity, and \u2014 for a token login \u2014 exactly what it's scoped to").action(authWhoamiCommand);
973
1553
  auth.command("users").description("List platform users").action(authUsersCommand);
974
1554
  auth.command("invite <email>").description("Invite a user by email").option("--admin", "Grant admin role").action(authInviteCommand);
1555
+ auth.command("ban <id>").description("Ban a platform user (blocks sign-in immediately, no session revocation needed)").option("--reason <text>", "Ban reason").option("--days <n>", "Expire the ban after n days (omit for indefinite)").option("-y, --yes", "Skip confirmation prompt").action(authBanCommand);
1556
+ auth.command("unban <id>").description("Lift a ban").action(authUnbanCommand);
975
1557
  auth.command("site-url [url]").description("Get or set the active project's Auth Site URL (where end-user confirmation-email links redirect back to)").option("--clear", "Clear the Site URL \u2014 confirmation links fall back to the project's own deployment URL").action(authSiteUrlCommand);
976
1558
  auth.command("stats").description("Show Auth-as-a-Service stats for the active project's end users").action(authStatsCommand);
977
1559
  var authProviders = auth.command("providers").description("Manage the active project's end-user sign-in providers");
@@ -996,6 +1578,58 @@ var deploy = program.command("deploy").description("Deployment commands");
996
1578
  deploy.command("trigger").description("Trigger a manual deployment").action(deployCommand);
997
1579
  deploy.command("status").description("Show latest deployment status").action(deployStatusCommand);
998
1580
  deploy.command("logs [deployment-id]").description("Show logs for a deployment (defaults to latest)").action(deployLogsCommand);
1581
+ var vps = program.command("vps").description("VPS compute hosting commands (raw Postgres/Redis, custom docker-compose deploys)");
1582
+ var vpsDatabase = vps.command("database").description("Raw Postgres (dedicated container, real connection string \u2014 for drizzle-kit, transactions, etc.)");
1583
+ vpsDatabase.command("create").description("Provision raw Postgres (idempotent; connection string shown once)").action(vpsDatabaseCreateCommand);
1584
+ vpsDatabase.command("info").description("Show raw Postgres connection info (no password)").action(vpsDatabaseInfoCommand);
1585
+ vpsDatabase.command("rotate").description("Rotate the raw Postgres password").option("--yes", "Skip confirmation prompt").action(vpsDatabaseRotateCommand);
1586
+ var vpsRedis = vps.command("redis").description("Raw Redis (dedicated container \u2014 for BullMQ, ioredis, Lua EVAL scripts)");
1587
+ vpsRedis.command("create").description("Provision raw Redis (idempotent; connection string shown once)").action(vpsRedisCreateCommand);
1588
+ vpsRedis.command("info").description("Show raw Redis connection info (no password)").action(vpsRedisInfoCommand);
1589
+ vpsRedis.command("rotate").description("Rotate the raw Redis password").option("--yes", "Skip confirmation prompt").action(vpsRedisRotateCommand);
1590
+ var vpsAllowlist = vps.command("allowlist").description("IP allowlist gating access to raw Postgres/Redis ports");
1591
+ vpsAllowlist.command("list").description("List allowlist entries").action(vpsAllowlistListCommand);
1592
+ vpsAllowlist.command("add <cidr>").description("Allow an IP or CIDR block (e.g. 203.0.113.4 or 203.0.113.0/24)").option("--label <label>", "Human-readable label").action(vpsAllowlistAddCommand);
1593
+ vpsAllowlist.command("remove <id>").description("Remove an allowlist entry").option("--yes", "Skip confirmation prompt").action(vpsAllowlistRemoveCommand);
1594
+ vps.command("env").description("List VPS-scoped env var names set on the active project (values are never returned)").action(vpsEnvCommand);
1595
+ vps.command("set-env <key> <value>").description("Set a VPS-scoped env var (e.g. JWT_SECRET) \u2014 distinct from 'hosting set-env', which sets Worker secrets").action(vpsSetEnvCommand);
1596
+ vps.command("deploy").description("Build and start the active project's docker-compose stack on the VPS").action(vpsDeployCommand);
1597
+ vps.command("deployments").description("List VPS deploy history").action(vpsDeploymentsCommand);
1598
+ vps.command("deploy-status [deployment-id]").description("Show a VPS deployment's status and logs (defaults to latest)").action(vpsDeployStatusCommand);
1599
+ vps.command("status").description("Show running compute services + raw Postgres/Redis container status").action(vpsStatusCommand);
1600
+ vps.command("logs [service]").description("Tail logs for a compute service (or 'postgres'/'redis')").option("--tail <n>", "Number of lines", "200").action(vpsLogsCommand);
1601
+ var github = program.command("github").description("GitHub account connection (OAuth)");
1602
+ github.command("status").description("Show whether your GitHub account is connected").action(githubStatusCommand);
1603
+ github.command("connect").description("Print the URL to open in a browser to connect GitHub").action(githubConnectCommand);
1604
+ github.command("repos").description("List repos your connected GitHub account can access").action(githubReposCommand);
1605
+ github.command("disconnect").description("Remove your stored GitHub OAuth token").option("-y, --yes", "Skip confirmation prompt").action(githubDisconnectCommand);
1606
+ var repo = program.command("repo").description("Active project's connected repo (auto-deploy from GitHub)");
1607
+ repo.command("status").description("Show the active project's linked repo").action(repoStatusCommand);
1608
+ repo.command("link <owner/repo>").description("Link a repo (requires 'deploro github connect' first)").option("--branch <branch>", "Branch to deploy from (default: main)").action(repoLinkCommand);
1609
+ repo.command("unlink").description("Unlink the repo and remove its GitHub webhook").option("-y, --yes", "Skip confirmation prompt").action(repoUnlinkCommand);
1610
+ repo.command("auto-deploy <on|off>").description("Toggle automatic deployment on push").action(repoAutoDeployCommand);
1611
+ var domains = program.command("domains").description("Custom domains for the active project");
1612
+ domains.command("list").description("List custom domains").action(domainsListCommand);
1613
+ domains.command("add <hostname>").description("Add a domain \u2014 prints the CNAME + TXT records to create").action(domainsAddCommand);
1614
+ domains.command("verify <id-or-hostname>").description("Verify DNS ownership and register SSL").action(domainsVerifyCommand);
1615
+ domains.command("status <id-or-hostname>").description("Poll domain verification/SSL status").action(domainsStatusCommand);
1616
+ domains.command("remove <id-or-hostname>").description("Remove a domain").option("-y, --yes", "Skip confirmation prompt").action(domainsRemoveCommand);
1617
+ var storage = program.command("storage").description("R2 file storage for the active project");
1618
+ 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);
1620
+ 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
+ storage.command("delete <key>").description("Delete a stored file").option("-y, --yes", "Skip confirmation prompt").action(storageDeleteCommand);
1622
+ var webhooks = program.command("webhooks").description("Outbound webhooks for the active project");
1623
+ webhooks.command("list").description("List webhooks").action(webhooksListCommand);
1624
+ 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);
1625
+ webhooks.command("update <id>").description("Update a webhook").option("--url <url>").option("--events <list>", "Comma-separated event names").option("--secret <secret>").option("--enable").option("--disable").action(webhooksUpdateCommand);
1626
+ webhooks.command("delete <id>").description("Delete a webhook").option("-y, --yes", "Skip confirmation prompt").action(webhooksDeleteCommand);
1627
+ webhooks.command("test <id>").description("Fire a test payload").action(webhooksTestCommand);
1628
+ webhooks.command("deliveries <id>").description("Show the last 30 delivery attempts").action(webhooksDeliveriesCommand);
1629
+ var team = program.command("team").description("Manage the active project's team members");
1630
+ team.command("list").description("List project members").action(teamListCommand);
1631
+ team.command("invite <email>").description("Add an existing Deploro user to this project").option("--admin", "Grant project admin role").action(teamInviteCommand);
1632
+ team.command("remove <member-id>").description("Remove a member from this project").option("-y, --yes", "Skip confirmation prompt").action(teamRemoveCommand);
999
1633
  program.parseAsync().catch((err) => {
1000
1634
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1001
1635
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploro",
3
- "version": "0.3.4",
3
+ "version": "0.5.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": {