@neta-art/cohub-cli 10.11.1 → 10.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -496,6 +496,15 @@ cohub apps versions <appId> --json
496
496
  cohub apps rm <appId> --yes
497
497
  ```
498
498
 
499
+ Bind a custom domain to a published public App. The binding costs a one-time $5, charged when the domain goes live; `add` prints the CNAME and TXT records to create at the DNS provider:
500
+
501
+ ```bash
502
+ cohub apps domains add <app> app.example.com --yes
503
+ cohub apps domains ls <app>
504
+ cohub apps domains check <app> app.example.com
505
+ cohub apps domains rm <app> app.example.com --yes
506
+ ```
507
+
499
508
  Resolve a published App by public identity:
500
509
 
501
510
  ```bash
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerAppDomains(appsCmd: Command): void;
@@ -0,0 +1,133 @@
1
+ import { normalizeAppDomainHostname } from "@neta-art/cohub";
2
+ import { createClient } from "../client.js";
3
+ import { getAppByRef } from "../app-ref.js";
4
+ import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
5
+ async function confirm(question, opts, hint) {
6
+ if (opts.yes)
7
+ return;
8
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
9
+ return error("Confirmation required", hint);
10
+ process.stdout.write(`${question} [y/N] `);
11
+ const chunks = [];
12
+ for await (const chunk of process.stdin) {
13
+ chunks.push(chunk);
14
+ break;
15
+ }
16
+ const answer = Buffer.concat(chunks).toString().trim().toLowerCase();
17
+ if (answer !== "y" && answer !== "yes")
18
+ return error("Cancelled");
19
+ }
20
+ async function findDomain(client, appId, ref) {
21
+ const { domains } = await client.apps.listDomains(appId);
22
+ const target = normalizeAppDomainHostname(ref) ?? ref.trim().toLowerCase();
23
+ const domain = domains.find((item) => item.id === ref || item.hostname === target);
24
+ return domain ?? error(`Domain not found: ${ref}`, "Run `cohub apps domains ls <app>` to see the App's domains.");
25
+ }
26
+ const info = (message) => console.log(message);
27
+ function printDomain(domain) {
28
+ table([{
29
+ hostname: domain.hostname,
30
+ status: domain.status,
31
+ certificate: domain.certificateStatus ?? "-",
32
+ error: domain.error?.message ?? "",
33
+ }], [
34
+ { key: "hostname", label: "Hostname" },
35
+ { key: "status", label: "Status" },
36
+ { key: "certificate", label: "Certificate" },
37
+ { key: "error", label: "Error" },
38
+ ]);
39
+ if (domain.status === "active")
40
+ return;
41
+ info("Add these DNS records at your DNS provider:");
42
+ table(domain.dnsRecords.map((record) => ({
43
+ ...record,
44
+ state: record.satisfied === true ? "ok" : record.satisfied === false ? "missing" : "-",
45
+ })), [
46
+ { key: "type", label: "Type" },
47
+ { key: "name", label: "Name" },
48
+ { key: "value", label: "Value" },
49
+ { key: "state", label: "State" },
50
+ ]);
51
+ }
52
+ export function registerAppDomains(appsCmd) {
53
+ const domainsCmd = appsCmd.command("domains").description("Custom domains for published Apps");
54
+ domainsCmd
55
+ .command("list <app>")
56
+ .alias("ls")
57
+ .description("List the App's custom domains")
58
+ .option("--json", "Output as JSON")
59
+ .action(async (appRef, opts) => {
60
+ const client = createClient();
61
+ try {
62
+ const detail = await getAppByRef(client, appRef);
63
+ const result = await client.apps.listDomains(detail.app.id);
64
+ if (jsonRequested(opts))
65
+ return outJson(result);
66
+ if (!result.enabled && result.domains.length === 0)
67
+ return info("Custom domains are not enabled on this deployment.");
68
+ if (result.domains.length === 0)
69
+ return info(`No custom domain. Add one with \`cohub apps domains add ${appRef} <hostname>\` ($${result.priceUsd}, one-time).`);
70
+ result.domains.forEach(printDomain);
71
+ }
72
+ catch (e) {
73
+ handleHttp(e);
74
+ }
75
+ });
76
+ domainsCmd
77
+ .command("add <app> <hostname>")
78
+ .description("Bind a custom domain; charged once when it goes live")
79
+ .option("-y, --yes", "Confirm the one-time charge")
80
+ .option("--json", "Output as JSON")
81
+ .action(async (appRef, hostname, opts) => {
82
+ const client = createClient();
83
+ try {
84
+ const detail = await getAppByRef(client, appRef);
85
+ const { priceUsd } = await client.apps.listDomains(detail.app.id);
86
+ await confirm(`Binding ${hostname} costs $${priceUsd}, charged once when the domain goes live. Continue?`, opts, `Pass --yes to accept the one-time $${priceUsd} charge.`);
87
+ const { domain } = await client.apps.addDomain(detail.app.id, hostname);
88
+ if (jsonRequested(opts))
89
+ return outJson(domain);
90
+ ok(`Domain added: ${domain.hostname}`);
91
+ printDomain(domain);
92
+ }
93
+ catch (e) {
94
+ handleHttp(e);
95
+ }
96
+ });
97
+ domainsCmd
98
+ .command("check <app> <domain>")
99
+ .description("Check DNS and certificate status now")
100
+ .option("--json", "Output as JSON")
101
+ .action(async (appRef, domainRef, opts) => {
102
+ const client = createClient();
103
+ try {
104
+ const detail = await getAppByRef(client, appRef);
105
+ const target = await findDomain(client, detail.app.id, domainRef);
106
+ const { domain } = await client.apps.checkDomain(detail.app.id, target.id);
107
+ if (jsonRequested(opts))
108
+ return outJson(domain);
109
+ printDomain(domain);
110
+ }
111
+ catch (e) {
112
+ handleHttp(e);
113
+ }
114
+ });
115
+ domainsCmd
116
+ .command("rm <app> <domain>")
117
+ .alias("remove")
118
+ .description("Remove a custom domain")
119
+ .option("-y, --yes", "Confirm removal")
120
+ .action(async (appRef, domainRef, opts) => {
121
+ const client = createClient();
122
+ try {
123
+ const detail = await getAppByRef(client, appRef);
124
+ const target = await findDomain(client, detail.app.id, domainRef);
125
+ await confirm(`Remove ${target.hostname}? The one-time charge is not refunded.`, opts, "Pass --yes to remove the domain.");
126
+ const result = await client.apps.removeDomain(detail.app.id, target.id);
127
+ ok(result.domain ? `Removing ${target.hostname}; its certificate is being released` : `Domain removed: ${target.hostname}`);
128
+ }
129
+ catch (e) {
130
+ handleHttp(e);
131
+ }
132
+ });
133
+ }
@@ -9,6 +9,7 @@ import { getAppByRef, parseAppRef } from "../app-ref.js";
9
9
  import { checkAppTarget } from "../app-target.js";
10
10
  import { putBytes, putLocalFile } from "../http-put.js";
11
11
  import { registerAppCommerce } from "./app-commerce.js";
12
+ import { registerAppDomains } from "./app-domains.js";
12
13
  import { collectPublicUpload } from "./public.js";
13
14
  const APP_STATUSES = ["published", "disabled"];
14
15
  const APP_VISIBILITIES = ["public", "space"];
@@ -722,6 +723,7 @@ export function registerApps(program) {
722
723
  }
723
724
  });
724
725
  registerAppCommerce(appsCmd);
726
+ registerAppDomains(appsCmd);
725
727
  // ── Viewer grants ─────────────────────────────────────────────────────────
726
728
  appsCmd
727
729
  .command("authorize <app>")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "10.11.1",
3
+ "version": "10.12.1",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -18,7 +18,7 @@
18
18
  "NOTICE"
19
19
  ],
20
20
  "dependencies": {
21
- "@neta-art/cohub": "12.4.0",
21
+ "@neta-art/cohub": "12.6.0",
22
22
  "@neta-art/generation": "^0.1.40",
23
23
  "commander": "^15.0.0",
24
24
  "sharp": "^0.35.4",