mdtolink 0.1.5 → 0.1.7

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as SERVER_URL } from "./config-B5jPCcGo.mjs";
2
+ import { n as SERVER_URL } from "./config-cFx42NU7.mjs";
3
3
 
4
4
  //#region ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v4/core/core.js
5
5
  /** A special constant with type `never` */
@@ -1297,7 +1297,7 @@ const $ZodBase64 = /* @__PURE__ */ $constructor("$ZodBase64", (inst, def) => {
1297
1297
  });
1298
1298
  function isValidBase64URL(data) {
1299
1299
  if (!base64url.test(data)) return false;
1300
- const base64$1 = data.replace(/[-_]/g, (c$5) => c$5 === "-" ? "+" : "/");
1300
+ const base64$1 = data.replace(/[-_]/g, (c$6) => c$6 === "-" ? "+" : "/");
1301
1301
  return isValidBase64(base64$1.padEnd(Math.ceil(base64$1.length / 4) * 4, "="));
1302
1302
  }
1303
1303
  const $ZodBase64URL = /* @__PURE__ */ $constructor("$ZodBase64URL", (inst, def) => {
@@ -7859,8 +7859,8 @@ const PaginationQuerySchema = object({
7859
7859
 
7860
7860
  //#endregion
7861
7861
  //#region ../../packages/contracts/src/contracts/billing.contract.ts
7862
- const c$4 = initContract();
7863
- const billingContract = c$4.router({
7862
+ const c$5 = initContract();
7863
+ const billingContract = c$5.router({
7864
7864
  getSubscription: {
7865
7865
  method: "GET",
7866
7866
  path: "/billing/subscription",
@@ -7940,13 +7940,13 @@ const DocumentListItemSchema = DocumentResponseSchema.omit({ content: true }).de
7940
7940
  const CreateDocumentRequestSchema = object({
7941
7941
  content: string().min(1).max(1e6).describe("Markdown content (max 1MB)"),
7942
7942
  title: string().max(200).optional().describe("Optional title (max 200 chars)"),
7943
- slug: string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional().describe("Custom URL slug (Pro+ only, lowercase alphanumeric and hyphens)"),
7943
+ slug: string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional().describe("Custom filename (Pro+ only, e.g., meeting-notes)"),
7944
7944
  isPublic: boolean().default(true).describe("Whether the document is publicly accessible")
7945
7945
  }).describe("Request body for creating a new document");
7946
7946
  const UpdateDocumentRequestSchema = object({
7947
7947
  content: string().min(1).max(1e6).optional().describe("Updated markdown content"),
7948
7948
  title: string().max(200).optional().describe("Updated title"),
7949
- slug: string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional().describe("Updated URL slug (Pro+ only)"),
7949
+ slug: string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional().describe("Updated filename (Pro+ only)"),
7950
7950
  isPublic: boolean().optional().describe("Updated visibility")
7951
7951
  }).describe("Request body for updating a document (all fields optional)");
7952
7952
  const PublicDocumentResponseSchema = object({
@@ -7964,8 +7964,8 @@ const UsernameDocumentParamsSchema = object({
7964
7964
 
7965
7965
  //#endregion
7966
7966
  //#region ../../packages/contracts/src/contracts/document.contract.ts
7967
- const c$3 = initContract();
7968
- const documentContract = c$3.router({
7967
+ const c$4 = initContract();
7968
+ const documentContract = c$4.router({
7969
7969
  list: {
7970
7970
  method: "GET",
7971
7971
  path: "/documents",
@@ -7993,7 +7993,7 @@ const documentContract = c$3.router({
7993
7993
  403: ErrorResponseSchema
7994
7994
  },
7995
7995
  summary: "Create document",
7996
- description: "Publishes a new markdown document to a shareable URL. Free tier users get nanoid-based URLs with 7-day expiry. Pro+ users can set custom slugs for user-scoped URLs (/@username/slug). Returns 403 if the user's plan document limit is reached."
7996
+ description: "Publishes a new markdown document to a shareable URL. Free tier users get nanoid-based URLs with 7-day expiry. Pro+ users can set custom filenames for user-scoped URLs (/@username/slug). Returns 403 if the user's plan document limit is reached."
7997
7997
  },
7998
7998
  get: {
7999
7999
  method: "GET",
@@ -8019,7 +8019,7 @@ const documentContract = c$3.router({
8019
8019
  404: ErrorResponseSchema
8020
8020
  },
8021
8021
  summary: "Update document",
8022
- description: "Updates a document's content, title, slug, or visibility. All fields are optional — only provided fields are updated. Custom slugs require Pro+ plan. Returns 400 if the slug is already taken."
8022
+ description: "Updates a document's content, title, slug, or visibility. All fields are optional — only provided fields are updated. Custom filenames require Pro+ plan. Returns 400 if the slug is already taken."
8023
8023
  },
8024
8024
  delete: {
8025
8025
  method: "DELETE",
@@ -8035,7 +8035,7 @@ const documentContract = c$3.router({
8035
8035
  description: "Soft-deletes a document by setting its status to 'deleted'. The document will no longer be accessible via its public URL. Requires authentication."
8036
8036
  }
8037
8037
  }, { pathPrefix: "/api" });
8038
- const publicDocumentContract = c$3.router({
8038
+ const publicDocumentContract = c$4.router({
8039
8039
  getBySlug: {
8040
8040
  method: "GET",
8041
8041
  path: "/d/:slug",
@@ -8058,10 +8058,84 @@ const publicDocumentContract = c$3.router({
8058
8058
  410: ErrorResponseSchema
8059
8059
  },
8060
8060
  summary: "Get document by username and slug",
8061
- description: "Retrieves a publicly shared document via a user-scoped URL (e.g. /@alice/my-doc). Available for Pro+ users who set custom slugs. Returns 410 if the document has expired. No authentication required."
8061
+ description: "Retrieves a publicly shared document via a user-scoped URL (e.g. /@alice/my-doc). Available for Pro+ users who set custom filenames. Returns 410 if the document has expired. No authentication required."
8062
8062
  }
8063
8063
  }, { pathPrefix: "" });
8064
8064
 
8065
+ //#endregion
8066
+ //#region ../../packages/contracts/src/schemas/domain.ts
8067
+ const DomainResponseSchema = object({
8068
+ id: string().describe("Domain ID"),
8069
+ domain: string().describe("Domain name"),
8070
+ userId: string().describe("Owner user ID"),
8071
+ verified: boolean().describe("Whether domain DNS has been verified"),
8072
+ verifiedAt: string().datetime().nullable().describe("Verification timestamp"),
8073
+ cnameTarget: string().describe("CNAME target the domain should point to"),
8074
+ dnsProvider: string().nullable().describe("Detected DNS provider name"),
8075
+ dnsManagementUrl: string().nullable().describe("Direct link to DNS management page"),
8076
+ createdAt: string().datetime().describe("Creation timestamp")
8077
+ }).describe("Custom domain with verification status");
8078
+ const CreateDomainRequestSchema = object({ domain: string().min(1).max(253).regex(/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/, "Must be a valid domain name (e.g., docs.example.com)").describe("Domain name to add") }).describe("Request body for adding a custom domain");
8079
+ const VerifyDomainResponseSchema = object({
8080
+ verified: boolean().describe("Whether verification succeeded"),
8081
+ message: string().describe("Verification result message")
8082
+ }).describe("Domain verification result");
8083
+
8084
+ //#endregion
8085
+ //#region ../../packages/contracts/src/contracts/domain.contract.ts
8086
+ const c$3 = initContract();
8087
+ const domainContract = c$3.router({
8088
+ list: {
8089
+ method: "GET",
8090
+ path: "/domains",
8091
+ responses: {
8092
+ 200: array(DomainResponseSchema),
8093
+ 401: ErrorResponseSchema
8094
+ },
8095
+ summary: "List custom domains",
8096
+ description: "Returns all custom domains for the authenticated user."
8097
+ },
8098
+ create: {
8099
+ method: "POST",
8100
+ path: "/domains",
8101
+ body: CreateDomainRequestSchema,
8102
+ responses: {
8103
+ 201: DomainResponseSchema,
8104
+ 400: ErrorResponseSchema,
8105
+ 401: ErrorResponseSchema,
8106
+ 403: ErrorResponseSchema
8107
+ },
8108
+ summary: "Add custom domain",
8109
+ description: "Adds a custom domain for the authenticated user. Requires Publisher plan. Returns DNS verification instructions. The domain must be globally unique."
8110
+ },
8111
+ verify: {
8112
+ method: "POST",
8113
+ path: "/domains/:id/verify",
8114
+ pathParams: object({ id: string().describe("Domain ID") }),
8115
+ body: null,
8116
+ responses: {
8117
+ 200: VerifyDomainResponseSchema,
8118
+ 401: ErrorResponseSchema,
8119
+ 404: ErrorResponseSchema
8120
+ },
8121
+ summary: "Verify domain DNS",
8122
+ description: "Checks DNS TXT record for the domain to verify ownership. Looks for a TXT record at _mdtolink-verification.{domain} matching the verification token."
8123
+ },
8124
+ delete: {
8125
+ method: "DELETE",
8126
+ path: "/domains/:id",
8127
+ pathParams: object({ id: string().describe("Domain ID") }),
8128
+ body: null,
8129
+ responses: {
8130
+ 200: SuccessResponseSchema,
8131
+ 401: ErrorResponseSchema,
8132
+ 404: ErrorResponseSchema
8133
+ },
8134
+ summary: "Remove custom domain",
8135
+ description: "Removes a custom domain from the authenticated user's account."
8136
+ }
8137
+ }, { pathPrefix: "/api" });
8138
+
8065
8139
  //#endregion
8066
8140
  //#region ../../packages/contracts/src/contracts/health.contract.ts
8067
8141
  const c$2 = initContract();
@@ -8151,6 +8225,7 @@ const c = initContract();
8151
8225
  const apiContract = c.router({
8152
8226
  billing: billingContract,
8153
8227
  documents: documentContract,
8228
+ domains: domainContract,
8154
8229
  health: healthContract,
8155
8230
  users: userContract
8156
8231
  });
@@ -7,19 +7,36 @@ import { homedir } from "node:os";
7
7
  const CONFIG_DIR = join(homedir(), ".config", "mdtolink");
8
8
  const TOKEN_PATH = join(CONFIG_DIR, "token.json");
9
9
  const DOCUMENTS_PATH = join(CONFIG_DIR, "documents.json");
10
+ const DEFAULTS_PATH = join(CONFIG_DIR, "defaults.json");
10
11
  const SERVER_URL = process.env.MDTOLINK_SERVER_URL || "https://api.mdtolink.com";
11
12
  const APP_URL = process.env.MDTOLINK_APP_URL || "https://app.mdtolink.com";
12
- async function getToken() {
13
+ async function readTokenData() {
13
14
  try {
14
15
  const raw = await readFile(TOKEN_PATH, "utf-8");
15
- return JSON.parse(raw).token ?? null;
16
+ return JSON.parse(raw);
16
17
  } catch {
17
- return null;
18
+ return {};
18
19
  }
19
20
  }
20
- async function storeToken(token) {
21
+ async function writeTokenData(data) {
21
22
  await mkdir(CONFIG_DIR, { recursive: true });
22
- await writeFile(TOKEN_PATH, JSON.stringify({ token }), { mode: 384 });
23
+ await writeFile(TOKEN_PATH, JSON.stringify(data), { mode: 384 });
24
+ }
25
+ async function getToken() {
26
+ return (await readTokenData()).token ?? null;
27
+ }
28
+ async function storeToken(token) {
29
+ const data = await readTokenData();
30
+ data.token = token;
31
+ await writeTokenData(data);
32
+ }
33
+ async function getUsername() {
34
+ return (await readTokenData()).username ?? null;
35
+ }
36
+ async function storeUsername(username) {
37
+ const data = await readTokenData();
38
+ data.username = username;
39
+ await writeTokenData(data);
23
40
  }
24
41
  async function clearToken() {
25
42
  try {
@@ -29,6 +46,18 @@ async function clearToken() {
29
46
  async function isLoggedIn() {
30
47
  return await getToken() !== null;
31
48
  }
49
+ async function getDefaults() {
50
+ try {
51
+ const raw = await readFile(DEFAULTS_PATH, "utf-8");
52
+ return JSON.parse(raw);
53
+ } catch {
54
+ return {};
55
+ }
56
+ }
57
+ async function setDefaults(defaults) {
58
+ await mkdir(CONFIG_DIR, { recursive: true });
59
+ await writeFile(DEFAULTS_PATH, JSON.stringify(defaults, null, " "), { mode: 384 });
60
+ }
32
61
  async function getDocumentMap() {
33
62
  try {
34
63
  const raw = await readFile(DOCUMENTS_PATH, "utf-8");
@@ -58,4 +87,4 @@ async function findMappingBySlug(slug) {
58
87
  }
59
88
 
60
89
  //#endregion
61
- export { getDocumentMap as a, removeDocumentMapping as c, findMappingBySlug as i, setDocumentMapping as l, SERVER_URL as n, getToken as o, clearToken as r, isLoggedIn as s, APP_URL as t, storeToken as u };
90
+ export { getDefaults as a, getUsername as c, setDefaults as d, setDocumentMapping as f, findMappingBySlug as i, isLoggedIn as l, storeUsername as m, SERVER_URL as n, getDocumentMap as o, storeToken as p, clearToken as r, getToken as s, APP_URL as t, removeDocumentMapping as u };
@@ -1868,4 +1868,4 @@ ${r ? import_picocolors.default.cyan(x) : ""}
1868
1868
  }).prompt();
1869
1869
 
1870
1870
  //#endregion
1871
- export { bt as a, Ze as i, Re as n, je as o, We as r, Ct as s, R as t };
1871
+ export { Ze as a, Ct as c, We as i, R as n, bt as o, Re as r, je as s, Je as t };
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ import "./api-CPJjLxqz.mjs";
3
+ import { a as getDefaults, d as setDefaults } from "./config-cFx42NU7.mjs";
4
+ import { c as Ct, n as R, o as bt, r as Re, t as Je } from "./dist-BbvD4qtQ.mjs";
5
+ import { n as requireAuth } from "./require-auth-m9LOildn.mjs";
6
+
7
+ //#region src/cli/commands/domain.ts
8
+ async function domainList() {
9
+ const auth = await requireAuth();
10
+ if (!auth) return;
11
+ const spin = bt();
12
+ spin.start("Fetching domains...");
13
+ const result = await auth.api.domains.list({});
14
+ if (result.status !== 200) {
15
+ spin.stop("Failed to fetch domains.");
16
+ process.exitCode = 1;
17
+ return;
18
+ }
19
+ spin.stop("Domains loaded.");
20
+ const domains = result.body;
21
+ if (domains.length === 0) {
22
+ R.info("No custom domains. Use `mdtl domain add <domain>` to add one.");
23
+ return;
24
+ }
25
+ const defaults = await getDefaults();
26
+ for (const domain of domains) {
27
+ const verified = domain.verified ? "verified" : "unverified";
28
+ const isDefault = defaults.defaultDomain === domain.domain ? " (default)" : "";
29
+ R.info(`${domain.domain} [${verified}]${isDefault}`);
30
+ if (!domain.verified) {
31
+ R.info(" Add a CNAME record to verify and route your domain:");
32
+ R.info(` ${domain.domain} → ${domain.cnameTarget}`);
33
+ if (domain.dnsManagementUrl) R.info(` ${domain.dnsProvider} DNS: ${domain.dnsManagementUrl}`);
34
+ }
35
+ }
36
+ }
37
+ async function domainAdd(domainName) {
38
+ const auth = await requireAuth();
39
+ if (!auth) return;
40
+ const spin = bt();
41
+ spin.start("Adding domain...");
42
+ const result = await auth.api.domains.create({ body: { domain: domainName } });
43
+ if (result.status === 201) {
44
+ const domain = result.body;
45
+ spin.stop("Domain added!");
46
+ R.success(`Added: ${domain.domain}`);
47
+ R.info("");
48
+ R.info("Add a CNAME record to verify and route your domain:");
49
+ R.info(` ${domain.domain} → ${domain.cnameTarget}`);
50
+ if (domain.dnsManagementUrl) {
51
+ R.info("");
52
+ R.info(`Open ${domain.dnsProvider ?? "your DNS provider"} to add the record:`);
53
+ R.info(` ${domain.dnsManagementUrl}`);
54
+ }
55
+ R.info("");
56
+ R.info(`Then run: mdtl domain verify ${domain.domain}`);
57
+ return;
58
+ }
59
+ spin.stop("Failed to add domain.");
60
+ const body = result.body;
61
+ R.error(body.error);
62
+ process.exitCode = 1;
63
+ }
64
+ async function domainVerify(domainName) {
65
+ const auth = await requireAuth();
66
+ if (!auth) return;
67
+ const listResult = await auth.api.domains.list({});
68
+ if (listResult.status !== 200) {
69
+ R.error("Failed to fetch domains.");
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+ const domain = listResult.body.find((d) => d.domain === domainName);
74
+ if (!domain) {
75
+ R.error(`Domain not found: ${domainName}`);
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ if (domain.verified) {
80
+ R.success(`${domainName} is already verified.`);
81
+ return;
82
+ }
83
+ const spin = bt();
84
+ spin.start("Checking DNS records...");
85
+ const result = await auth.api.domains.verify({ params: { id: domain.id } });
86
+ if (result.status !== 200) {
87
+ spin.stop("Verification failed.");
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+ if (result.body.verified) {
92
+ spin.stop("Verified!");
93
+ R.success(result.body.message);
94
+ } else {
95
+ spin.stop("Not verified yet.");
96
+ R.warning(result.body.message);
97
+ }
98
+ }
99
+ async function domainRemove(domainName) {
100
+ const auth = await requireAuth();
101
+ if (!auth) return;
102
+ const listResult = await auth.api.domains.list({});
103
+ if (listResult.status !== 200) {
104
+ R.error("Failed to fetch domains.");
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+ const domain = listResult.body.find((d) => d.domain === domainName);
109
+ if (!domain) {
110
+ R.error(`Domain not found: ${domainName}`);
111
+ process.exitCode = 1;
112
+ return;
113
+ }
114
+ const shouldDelete = await Re({ message: `Remove ${domainName}? This cannot be undone.` });
115
+ if (Ct(shouldDelete) || !shouldDelete) {
116
+ R.info("Cancelled.");
117
+ return;
118
+ }
119
+ if ((await auth.api.domains.delete({ params: { id: domain.id } })).status === 200) {
120
+ R.success(`Removed: ${domainName}`);
121
+ if ((await getDefaults()).defaultDomain === domainName) await setDefaults({ defaultDomain: void 0 });
122
+ } else {
123
+ R.error("Failed to remove domain.");
124
+ process.exitCode = 1;
125
+ }
126
+ }
127
+ async function domainDefault() {
128
+ const auth = await requireAuth();
129
+ if (!auth) return;
130
+ const listResult = await auth.api.domains.list({});
131
+ if (listResult.status !== 200) {
132
+ R.error("Failed to fetch domains.");
133
+ process.exitCode = 1;
134
+ return;
135
+ }
136
+ const verifiedDomains = listResult.body.filter((d) => d.verified);
137
+ if (verifiedDomains.length === 0) {
138
+ R.info("No verified domains. Add and verify a domain first.");
139
+ return;
140
+ }
141
+ const defaults = await getDefaults();
142
+ const selected = await Je({
143
+ message: "Select default domain:",
144
+ options: [{
145
+ value: "__none__",
146
+ label: "None (no default domain)"
147
+ }, ...verifiedDomains.map((d) => ({
148
+ value: d.domain,
149
+ label: d.domain,
150
+ hint: defaults.defaultDomain === d.domain ? "current default" : void 0
151
+ }))],
152
+ initialValue: defaults.defaultDomain ?? "__none__"
153
+ });
154
+ if (Ct(selected)) {
155
+ R.info("Cancelled.");
156
+ return;
157
+ }
158
+ const newDefault = selected === "__none__" ? void 0 : selected;
159
+ await setDefaults({ defaultDomain: newDefault });
160
+ if (newDefault) R.success(`Default domain set to: ${newDefault}`);
161
+ else R.success("Default domain cleared.");
162
+ }
163
+
164
+ //#endregion
165
+ export { domainAdd, domainDefault, domainList, domainRemove, domainVerify };
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { t as APP_URL } from "./config-B5jPCcGo.mjs";
2
+ import { t as APP_URL } from "./config-cFx42NU7.mjs";
3
3
 
4
4
  //#region src/cli/lib/format.ts
5
- function formatDocumentUrl(doc) {
5
+ function formatDocumentUrl(doc, username) {
6
+ if (doc.urlType === "user_scoped" && username) return `${APP_URL}/@${username}/${doc.slug}`;
6
7
  return `${APP_URL}/d/${doc.slug}`;
7
8
  }
8
- function formatDocumentTable(items, total, offset) {
9
+ function formatDocumentTable(items, total, offset, username) {
9
10
  if (items.length === 0) return "No documents found.";
10
11
  const lines = [];
11
12
  const header = padColumns([
@@ -21,7 +22,7 @@ function formatDocumentTable(items, total, offset) {
21
22
  for (const doc of items) {
22
23
  const title = doc.title || "(untitled)";
23
24
  const created = new Date(doc.createdAt).toLocaleDateString();
24
- const link = formatDocumentUrl(doc);
25
+ const link = formatDocumentUrl(doc, username);
25
26
  lines.push(padColumns([
26
27
  truncate(title, 30),
27
28
  truncate(doc.slug, 20),
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import "./api-CPJjLxqz.mjs";
3
+ import { m as storeUsername, n as SERVER_URL } from "./config-cFx42NU7.mjs";
4
+ import { n as R, o as bt } from "./dist-BbvD4qtQ.mjs";
5
+ import { n as requireAuth } from "./require-auth-m9LOildn.mjs";
6
+
7
+ //#region src/cli/commands/handle.ts
8
+ async function handleShow() {
9
+ const auth = await requireAuth();
10
+ if (!auth) return;
11
+ const { api } = auth;
12
+ const result = await api.users.getMe({});
13
+ if (result.status !== 200) {
14
+ R.error("Failed to fetch profile.");
15
+ process.exitCode = 1;
16
+ return;
17
+ }
18
+ const username = result.body.username;
19
+ if (username) {
20
+ await storeUsername(username);
21
+ R.info(`Your handle: @${username}`);
22
+ } else R.info("No handle set. Use `mdtl handle set <name>` to claim one.");
23
+ }
24
+ async function handleSet(name) {
25
+ const auth = await requireAuth();
26
+ if (!auth) return;
27
+ const { token } = auth;
28
+ const handle = name.startsWith("@") ? name.slice(1) : name;
29
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(handle)) {
30
+ R.error("Invalid handle. Use lowercase letters, numbers, hyphens, and underscores. Must start with a letter or number.");
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ if (handle.length < 3 || handle.length > 30) {
35
+ R.error("Handle must be between 3 and 30 characters.");
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ const spin = bt();
40
+ spin.start("Setting handle...");
41
+ const response = await fetch(`${SERVER_URL}/api/auth/update-user`, {
42
+ method: "POST",
43
+ headers: {
44
+ "Content-Type": "application/json",
45
+ Authorization: `Bearer ${token}`
46
+ },
47
+ body: JSON.stringify({ username: handle })
48
+ });
49
+ if (!response.ok) {
50
+ const body = await response.json();
51
+ const message = typeof body.message === "string" ? body.message : "Failed to set handle. It may already be taken.";
52
+ spin.stop("Failed.");
53
+ R.error(message);
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ await storeUsername(handle);
58
+ spin.stop("Done!");
59
+ R.success(`Handle set to @${handle}`);
60
+ }
61
+ async function handleRemove() {
62
+ const auth = await requireAuth();
63
+ if (!auth) return;
64
+ const { token } = auth;
65
+ if (!(await fetch(`${SERVER_URL}/api/auth/update-user`, {
66
+ method: "POST",
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ Authorization: `Bearer ${token}`
70
+ },
71
+ body: JSON.stringify({ username: "" })
72
+ })).ok) {
73
+ R.error("Failed to remove handle.");
74
+ process.exitCode = 1;
75
+ return;
76
+ }
77
+ await storeUsername("");
78
+ R.success("Handle removed.");
79
+ }
80
+
81
+ //#endregion
82
+ export { handleRemove, handleSet, handleShow };
package/dist/index.mjs CHANGED
@@ -3030,15 +3030,15 @@ const { program: program$1, createCommand, createArgument, createOption, Command
3030
3030
  const program = new Command();
3031
3031
  program.name("mdtolink").description("Publish markdown files to shareable URLs").version("0.1.0");
3032
3032
  program.command("login").aliases(["auth", "signin"]).description("Authenticate with MDtolink").action(async () => {
3033
- const { login } = await import("./login-DKrVl_-X.mjs");
3033
+ const { login } = await import("./login-DOLV04ZV.mjs");
3034
3034
  await login();
3035
3035
  });
3036
3036
  program.command("logout").aliases(["signout"]).description("Clear stored authentication token").action(async () => {
3037
- const { logout } = await import("./logout-2ajJT8D-.mjs");
3037
+ const { logout } = await import("./logout-t5CtUkc2.mjs");
3038
3038
  await logout();
3039
3039
  });
3040
- program.command("publish").aliases(["p", "pub"]).description("Publish or update a markdown file").argument("<file>", "Path to markdown file").option("--slug <slug>", "Custom URL slug (Pro+ only)").option("--title <title>", "Custom title (defaults to filename)").action(async (file, options) => {
3041
- const { publish } = await import("./publish-S5e-jsL5.mjs");
3040
+ program.command("publish").aliases(["p", "pub"]).description("Publish or update a markdown file").argument("<file>", "Path to markdown file").option("--slug <slug>", "Custom filename (Pro+ only, e.g., meeting-notes)").option("--title <title>", "Custom title (defaults to filename)").action(async (file, options) => {
3041
+ const { publish } = await import("./publish-DdAgmsVg.mjs");
3042
3042
  await publish(file, options);
3043
3043
  });
3044
3044
  program.command("unpublish").aliases([
@@ -3046,28 +3046,70 @@ program.command("unpublish").aliases([
3046
3046
  "remove",
3047
3047
  "delete"
3048
3048
  ]).description("Remove a published document").argument("[file-or-slug]", "File path or document slug").action(async (fileOrSlug) => {
3049
- const { unpublish } = await import("./unpublish-DguElyLU.mjs");
3049
+ const { unpublish } = await import("./unpublish-DqwzN793.mjs");
3050
3050
  await unpublish(fileOrSlug);
3051
3051
  });
3052
3052
  program.command("list").aliases(["ls", "l"]).description("List published documents").option("--limit <n>", "Number of documents to show", "20").option("--offset <n>", "Skip N documents", "0").action(async (options) => {
3053
- const { list } = await import("./list-Dv3o-Yt-.mjs");
3053
+ const { list } = await import("./list-5TKm4hrp.mjs");
3054
3054
  await list({
3055
3055
  limit: Number.parseInt(options.limit, 10),
3056
3056
  offset: Number.parseInt(options.offset, 10)
3057
3057
  });
3058
3058
  });
3059
+ const handleCmd = program.command("handle").aliases(["h"]).description("Manage your handle (@username)");
3060
+ handleCmd.command("show").description("Show current handle").action(async () => {
3061
+ const { handleShow } = await import("./handle-C-teVKcx.mjs");
3062
+ await handleShow();
3063
+ });
3064
+ handleCmd.command("set").argument("<name>", "Handle to claim (e.g., david)").description("Set your handle").action(async (name) => {
3065
+ const { handleSet } = await import("./handle-C-teVKcx.mjs");
3066
+ await handleSet(name);
3067
+ });
3068
+ handleCmd.command("remove").description("Remove your handle").action(async () => {
3069
+ const { handleRemove } = await import("./handle-C-teVKcx.mjs");
3070
+ await handleRemove();
3071
+ });
3072
+ handleCmd.action(async () => {
3073
+ const { handleShow } = await import("./handle-C-teVKcx.mjs");
3074
+ await handleShow();
3075
+ });
3076
+ const domainCmd = program.command("domain").aliases(["dom"]).description("Manage custom domains");
3077
+ domainCmd.command("list").description("List your custom domains").action(async () => {
3078
+ const { domainList } = await import("./domain-D5HsJ52R.mjs");
3079
+ await domainList();
3080
+ });
3081
+ domainCmd.command("add").argument("<domain>", "Domain name (e.g., docs.example.com)").description("Add a custom domain").action(async (domain) => {
3082
+ const { domainAdd } = await import("./domain-D5HsJ52R.mjs");
3083
+ await domainAdd(domain);
3084
+ });
3085
+ domainCmd.command("verify").argument("<domain>", "Domain name to verify").description("Verify domain DNS ownership").action(async (domain) => {
3086
+ const { domainVerify } = await import("./domain-D5HsJ52R.mjs");
3087
+ await domainVerify(domain);
3088
+ });
3089
+ domainCmd.command("remove").argument("<domain>", "Domain name to remove").description("Remove a custom domain").action(async (domain) => {
3090
+ const { domainRemove } = await import("./domain-D5HsJ52R.mjs");
3091
+ await domainRemove(domain);
3092
+ });
3093
+ domainCmd.command("default").description("Set default custom domain").action(async () => {
3094
+ const { domainDefault } = await import("./domain-D5HsJ52R.mjs");
3095
+ await domainDefault();
3096
+ });
3097
+ domainCmd.action(async () => {
3098
+ const { domainList } = await import("./domain-D5HsJ52R.mjs");
3099
+ await domainList();
3100
+ });
3059
3101
  program.command("commands").description("Show all available commands").action(() => {
3060
3102
  printCommands();
3061
3103
  });
3062
3104
  program.argument("[file]", "Markdown file to publish (shorthand for `publish`)");
3063
- program.option("--slug <slug>", "Custom URL slug (Pro+ only)");
3105
+ program.option("--slug <slug>", "Custom filename (Pro+ only, e.g., meeting-notes)");
3064
3106
  program.option("--title <title>", "Custom title (defaults to filename)");
3065
3107
  program.action(async (file, options) => {
3066
- if (!file) {
3108
+ if (!file || file === "help") {
3067
3109
  program.help();
3068
3110
  return;
3069
3111
  }
3070
- const { publish } = await import("./publish-S5e-jsL5.mjs");
3112
+ const { publish } = await import("./publish-DdAgmsVg.mjs");
3071
3113
  await publish(file, options);
3072
3114
  });
3073
3115
  program.addHelpText("after", () => {
@@ -3077,14 +3119,15 @@ program.addHelpText("after", () => {
3077
3119
  Aliases:
3078
3120
  publish p, pub login auth, signin
3079
3121
  unpublish rm, remove logout signout
3080
- list ls, l`;
3122
+ list ls, l handle h
3123
+ domain dom`;
3081
3124
  });
3082
3125
  function printCommands() {
3083
3126
  console.log(`
3084
3127
  mdtolink commands:
3085
3128
 
3086
3129
  publish <file> Publish or update a markdown file
3087
- --slug <slug> Custom URL slug (Pro+ only)
3130
+ --slug <name> Custom filename (Pro+ only, e.g., meeting-notes)
3088
3131
  --title <title> Custom title (defaults to filename)
3089
3132
 
3090
3133
  unpublish [target] Remove a published document
@@ -3092,6 +3135,16 @@ mdtolink commands:
3092
3135
  --limit <n> Number of documents to show (default: 20)
3093
3136
  --offset <n> Skip N documents (default: 0)
3094
3137
 
3138
+ handle Show your handle (@username)
3139
+ handle set <name> Set your handle (e.g., mdtl handle set david)
3140
+ handle remove Remove your handle
3141
+
3142
+ domain List your custom domains
3143
+ domain add <domain> Add a custom domain (Publisher only)
3144
+ domain verify <dom> Verify domain DNS ownership
3145
+ domain remove <dom> Remove a custom domain
3146
+ domain default Set default custom domain
3147
+
3095
3148
  login Authenticate with MDtolink
3096
3149
  logout Clear stored authentication token
3097
3150
  commands Show this list
@@ -3102,7 +3155,8 @@ Shortcuts:
3102
3155
  Aliases:
3103
3156
  publish p, pub login auth, signin
3104
3157
  unpublish rm, remove logout signout
3105
- list ls, l
3158
+ list ls, l handle h
3159
+ domain dom
3106
3160
  `);
3107
3161
  }
3108
3162
  await program.parseAsync();
@@ -1,18 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { t as createApiClient } from "./api-D4MGz1n7.mjs";
3
- import { o as getToken } from "./config-B5jPCcGo.mjs";
4
- import { t as formatDocumentTable } from "./format-DbYC9f36.mjs";
5
- import { a as bt, t as R } from "./dist-DVBd1E-5.mjs";
2
+ import "./api-CPJjLxqz.mjs";
3
+ import "./config-cFx42NU7.mjs";
4
+ import { t as formatDocumentTable } from "./format-CaRgIpjB.mjs";
5
+ import { n as R, o as bt } from "./dist-BbvD4qtQ.mjs";
6
+ import { n as requireAuth } from "./require-auth-m9LOildn.mjs";
6
7
 
7
8
  //#region src/cli/commands/list.ts
8
9
  async function list(options) {
9
- const token = await getToken();
10
- if (!token) {
11
- R.error("Not logged in. Run `mdtolink login` first.");
12
- process.exitCode = 1;
13
- return;
14
- }
15
- const api = createApiClient(token);
10
+ const auth = await requireAuth();
11
+ if (!auth) return;
12
+ const { api, username } = auth;
16
13
  const spin = bt();
17
14
  spin.start("Fetching documents...");
18
15
  try {
@@ -23,7 +20,7 @@ async function list(options) {
23
20
  if (result.status === 200) {
24
21
  spin.stop("Documents loaded.");
25
22
  const { items, total, offset } = result.body;
26
- const output = formatDocumentTable(items, total, offset);
23
+ const output = formatDocumentTable(items, total, offset, username);
27
24
  R.info(output);
28
25
  } else {
29
26
  const body = result.body;
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { n as SERVER_URL, u as storeToken } from "./config-B5jPCcGo.mjs";
3
- import { a as bt, r as We, t as R } from "./dist-DVBd1E-5.mjs";
2
+ import { t as createApiClient } from "./api-CPJjLxqz.mjs";
3
+ import { m as storeUsername, n as SERVER_URL, p as storeToken } from "./config-cFx42NU7.mjs";
4
+ import { i as We, n as R, o as bt } from "./dist-BbvD4qtQ.mjs";
4
5
 
5
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/broadcast-channel.mjs
6
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/broadcast-channel.mjs
6
7
  const kBroadcastChannel = Symbol.for("better-auth:broadcast-channel");
7
8
  const now$1 = () => Math.floor(Date.now() / 1e3);
8
9
  var WindowBroadcastChannel = class {
@@ -46,7 +47,7 @@ function getGlobalBroadcastChannel(name = "better-auth.message") {
46
47
  }
47
48
 
48
49
  //#endregion
49
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/focus-manager.mjs
50
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/focus-manager.mjs
50
51
  const kFocusManager = Symbol.for("better-auth:focus-manager");
51
52
  var WindowFocusManager = class {
52
53
  listeners = /* @__PURE__ */ new Set();
@@ -76,7 +77,7 @@ function getGlobalFocusManager() {
76
77
  }
77
78
 
78
79
  //#endregion
79
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/online-manager.mjs
80
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/online-manager.mjs
80
81
  const kOnlineManager = Symbol.for("better-auth:online-manager");
81
82
  var WindowOnlineManager = class {
82
83
  listeners = /* @__PURE__ */ new Set();
@@ -240,7 +241,7 @@ let onMount = ($store, initialize) => {
240
241
  };
241
242
 
242
243
  //#endregion
243
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/query.mjs
244
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/query.mjs
244
245
  const isServer = () => typeof window === "undefined";
245
246
  const useAuthQuery = (initializedAtom, path, $fetch, options) => {
246
247
  const value = /* @__PURE__ */ atom({
@@ -334,7 +335,7 @@ const useAuthQuery = (initializedAtom, path, $fetch, options) => {
334
335
  };
335
336
 
336
337
  //#endregion
337
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/session-refresh.mjs
338
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/session-refresh.mjs
338
339
  const now = () => Math.floor(Date.now() / 1e3);
339
340
  /**
340
341
  * Rate limit: don't refetch on focus if a session request was made within this many seconds
@@ -753,7 +754,7 @@ var BetterAuthError = class extends Error {
753
754
  };
754
755
 
755
756
  //#endregion
756
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/utils/url.mjs
757
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/utils/url.mjs
757
758
  function checkHasPath(url) {
758
759
  try {
759
760
  return (new URL(url).pathname.replace(/\/+$/, "") || "/") !== "/";
@@ -826,7 +827,7 @@ function getOrigin(url) {
826
827
  }
827
828
 
828
829
  //#endregion
829
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/fetch-plugins.mjs
830
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/fetch-plugins.mjs
830
831
  const redirectPlugin = {
831
832
  id: "redirect",
832
833
  name: "Redirect",
@@ -842,7 +843,7 @@ const redirectPlugin = {
842
843
  };
843
844
 
844
845
  //#endregion
845
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/parser.mjs
846
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/parser.mjs
846
847
  const PROTO_POLLUTION_PATTERNS = {
847
848
  proto: /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/,
848
849
  constructor: /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/,
@@ -913,7 +914,7 @@ function parseJSON(value, options = { strict: true }) {
913
914
  }
914
915
 
915
916
  //#endregion
916
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/session-atom.mjs
917
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/session-atom.mjs
917
918
  function getSessionAtom($fetch, options) {
918
919
  const $signal = /* @__PURE__ */ atom(false);
919
920
  const session = useAuthQuery($signal, "/get-session", $fetch, { method: "GET" });
@@ -1412,7 +1413,7 @@ var betterFetch = async (url, options) => {
1412
1413
  };
1413
1414
 
1414
1415
  //#endregion
1415
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/config.mjs
1416
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/config.mjs
1416
1417
  const getClientConfig = (options, loadEnv) => {
1417
1418
  const isCredentialsSupported = "credentials" in Request.prototype;
1418
1419
  const baseURL = getBaseURL(options?.baseURL, options?.basePath, void 0, loadEnv) ?? "/api/auth";
@@ -1493,13 +1494,13 @@ const getClientConfig = (options, loadEnv) => {
1493
1494
  };
1494
1495
 
1495
1496
  //#endregion
1496
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/utils/is-atom.mjs
1497
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/utils/is-atom.mjs
1497
1498
  function isAtom(value) {
1498
1499
  return typeof value === "object" && value !== null && "get" in value && typeof value.get === "function" && "lc" in value && typeof value.lc === "number";
1499
1500
  }
1500
1501
 
1501
1502
  //#endregion
1502
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/proxy.mjs
1503
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/proxy.mjs
1503
1504
  function getMethod(path, knownPathMethods, args) {
1504
1505
  const method = knownPathMethods[path];
1505
1506
  const { fetchOptions, query: _query, ...body } = args || {};
@@ -1574,7 +1575,7 @@ function createDynamicPathProxy(routes, client, knownPathMethods, atoms, atomLis
1574
1575
  }
1575
1576
 
1576
1577
  //#endregion
1577
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/client/vanilla.mjs
1578
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/client/vanilla.mjs
1578
1579
  function createAuthClient(options) {
1579
1580
  const { pluginPathMethods, pluginsActions, pluginsAtoms, $fetch, atomListeners, $store } = getClientConfig(options);
1580
1581
  const resolvedHooks = {};
@@ -1588,7 +1589,7 @@ function createAuthClient(options) {
1588
1589
  }
1589
1590
 
1590
1591
  //#endregion
1591
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/plugins/access/access.mjs
1592
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/plugins/access/access.mjs
1592
1593
  function role(statements) {
1593
1594
  return {
1594
1595
  authorize(request, connector = "AND") {
@@ -1630,7 +1631,7 @@ function createAccessControl(s) {
1630
1631
  }
1631
1632
 
1632
1633
  //#endregion
1633
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/plugins/admin/access/statement.mjs
1634
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/plugins/admin/access/statement.mjs
1634
1635
  const defaultStatements$1 = {
1635
1636
  user: [
1636
1637
  "create",
@@ -1674,7 +1675,7 @@ const userAc = defaultAc$1.newRole({
1674
1675
  });
1675
1676
 
1676
1677
  //#endregion
1677
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/plugins/device-authorization/client.mjs
1678
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/plugins/device-authorization/client.mjs
1678
1679
  const deviceAuthorizationClient = () => {
1679
1680
  return {
1680
1681
  id: "device-authorization",
@@ -1690,7 +1691,7 @@ const deviceAuthorizationClient = () => {
1690
1691
  };
1691
1692
 
1692
1693
  //#endregion
1693
- //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_6e9d8bfe34916fc4fa1d1185a8201c1b/node_modules/better-auth/dist/plugins/organization/access/statement.mjs
1694
+ //#region ../../node_modules/.pnpm/better-auth@1.4.20_drizzle-kit@0.31.9_drizzle-orm@0.45.1_@opentelemetry+api@1.9.0_@type_a40a2e15f82eb076fc233026dff617a5/node_modules/better-auth/dist/plugins/organization/access/statement.mjs
1694
1695
  const defaultStatements = {
1695
1696
  organization: ["update", "delete"],
1696
1697
  member: [
@@ -1784,6 +1785,10 @@ async function pollForToken(deviceCode, interval) {
1784
1785
  const userName = session?.user?.name || "there";
1785
1786
  R.success(`Authenticated successfully! Hello, ${userName}!`);
1786
1787
  R.info("Token stored at ~/.config/mdtolink/token.json");
1788
+ try {
1789
+ const result = await createApiClient(data.access_token).users.getMe({});
1790
+ if (result.status === 200 && result.body.username) await storeUsername(result.body.username);
1791
+ } catch {}
1787
1792
  return;
1788
1793
  }
1789
1794
  if (error) {
@@ -1824,7 +1829,7 @@ async function login() {
1824
1829
  R.info(`Your code: ${user_code}`);
1825
1830
  R.info(`Visit: ${verification_uri}`);
1826
1831
  try {
1827
- const { default: open } = await import("./open-CZUGhT0Q.mjs");
1832
+ const { default: open } = await import("./open-DQe1i6ko.mjs");
1828
1833
  const urlToOpen = verification_uri_complete || verification_uri;
1829
1834
  if (urlToOpen) {
1830
1835
  await open(urlToOpen);
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { r as clearToken, s as isLoggedIn } from "./config-B5jPCcGo.mjs";
3
- import { t as R } from "./dist-DVBd1E-5.mjs";
2
+ import { l as isLoggedIn, r as clearToken } from "./config-cFx42NU7.mjs";
3
+ import { n as R } from "./dist-BbvD4qtQ.mjs";
4
4
 
5
5
  //#region src/cli/commands/logout.ts
6
6
  async function logout() {
package/dist/mcp.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { r as __toESM, t as __commonJSMin } from "./chunk-DrUt9iA_.mjs";
3
- import { A as clone, C as meta$1, D as parseAsync, E as parse$1, M as normalizeParams, N as $constructor, O as safeParse$1, S as describe$1, T as $ZodType, _ as string, a as array, b as datetime, c as discriminatedUnion, d as looseObject, f as number, g as record, h as preprocess, i as _null, j as defineLazy, k as safeParseAsync$1, l as intersection, m as optional, n as ZodOptional, o as boolean, p as object$1, r as _enum, s as custom, t as createApiClient, u as literal, v as union, w as $ZodObject, x as toJSONSchema, y as unknown } from "./api-D4MGz1n7.mjs";
4
- import { o as getToken } from "./config-B5jPCcGo.mjs";
5
- import { n as formatDocumentUrl } from "./format-DbYC9f36.mjs";
3
+ import { A as clone, C as meta$1, D as parseAsync, E as parse$1, M as normalizeParams, N as $constructor, O as safeParse$1, S as describe$1, T as $ZodType, _ as string, a as array, b as datetime, c as discriminatedUnion, d as looseObject, f as number, g as record, h as preprocess, i as _null, j as defineLazy, k as safeParseAsync$1, l as intersection, m as optional, n as ZodOptional, o as boolean, p as object$1, r as _enum, s as custom, t as createApiClient, u as literal, v as union, w as $ZodObject, x as toJSONSchema, y as unknown } from "./api-CPJjLxqz.mjs";
4
+ import { s as getToken } from "./config-cFx42NU7.mjs";
5
+ import { n as formatDocumentUrl } from "./format-CaRgIpjB.mjs";
6
6
  import N from "node:process";
7
7
 
8
8
  //#region ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/helpers/util.js
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { t as createApiClient } from "./api-D4MGz1n7.mjs";
3
- import { a as getDocumentMap, l as setDocumentMapping, o as getToken } from "./config-B5jPCcGo.mjs";
4
- import { n as formatDocumentUrl } from "./format-DbYC9f36.mjs";
5
- import { a as bt, n as Re, s as Ct, t as R } from "./dist-DVBd1E-5.mjs";
2
+ import "./api-CPJjLxqz.mjs";
3
+ import { f as setDocumentMapping, o as getDocumentMap } from "./config-cFx42NU7.mjs";
4
+ import { n as formatDocumentUrl } from "./format-CaRgIpjB.mjs";
5
+ import { n as R, o as bt } from "./dist-BbvD4qtQ.mjs";
6
+ import { n as requireAuth, t as promptReauth } from "./require-auth-m9LOildn.mjs";
6
7
  import { basename, extname, resolve } from "node:path";
7
8
  import { access, readFile } from "node:fs/promises";
8
9
 
@@ -16,22 +17,8 @@ async function publish(filePath, options) {
16
17
  process.exitCode = 1;
17
18
  return;
18
19
  }
19
- let token = await getToken();
20
- if (!token) {
21
- const shouldLogin = await Re({ message: "You are not logged in. Would you like to authorize now?" });
22
- if (Ct(shouldLogin) || !shouldLogin) {
23
- R.info("Publish cancelled. Run `mdtolink login` to authenticate.");
24
- return;
25
- }
26
- const { login } = await import("./login-DKrVl_-X.mjs");
27
- await login();
28
- token = await getToken();
29
- if (!token) {
30
- R.error("Authentication failed. Cannot publish.");
31
- process.exitCode = 1;
32
- return;
33
- }
34
- }
20
+ const auth = await requireAuth();
21
+ if (!auth) return;
35
22
  const content = await readFile(absolutePath, "utf-8");
36
23
  if (content.trim().length === 0) {
37
24
  R.error("File is empty.");
@@ -40,17 +27,36 @@ async function publish(filePath, options) {
40
27
  }
41
28
  const title = options.title ?? basename(absolutePath, extname(absolutePath));
42
29
  const existingMapping = (await getDocumentMap())[absolutePath];
43
- const api = createApiClient(token);
30
+ let { api, username } = auth;
44
31
  const spin = bt();
45
32
  if (existingMapping) {
46
33
  spin.start("Updating document...");
47
- await updateExisting(api, absolutePath, existingMapping.documentId, content, title, options.slug, spin);
34
+ if (await updateExisting(api, absolutePath, existingMapping.documentId, content, title, options.slug, spin, username)) {
35
+ const reauthed = await promptReauth();
36
+ if (!reauthed) return;
37
+ api = reauthed.api;
38
+ username = reauthed.username;
39
+ const retrySpin = bt();
40
+ retrySpin.start("Retrying update...");
41
+ await updateExisting(api, absolutePath, existingMapping.documentId, content, title, options.slug, retrySpin, username);
42
+ }
48
43
  } else {
49
44
  spin.start("Publishing document...");
50
- await publishNew(api, absolutePath, content, title, options.slug, spin);
45
+ if (await publishNew(api, absolutePath, content, title, options.slug, spin, username)) {
46
+ const reauthed = await promptReauth();
47
+ if (!reauthed) return;
48
+ api = reauthed.api;
49
+ username = reauthed.username;
50
+ const retrySpin = bt();
51
+ retrySpin.start("Retrying publish...");
52
+ await publishNew(api, absolutePath, content, title, options.slug, retrySpin, username);
53
+ }
51
54
  }
52
55
  }
53
- async function updateExisting(api, absolutePath, documentId, content, title, slug, spin) {
56
+ /**
57
+ * Returns true if the caller should prompt re-auth and retry.
58
+ */
59
+ async function updateExisting(api, absolutePath, documentId, content, title, slug, spin, username) {
54
60
  try {
55
61
  const result = await api.documents.update({
56
62
  params: { id: documentId },
@@ -62,7 +68,7 @@ async function updateExisting(api, absolutePath, documentId, content, title, slu
62
68
  });
63
69
  if (result.status === 200) {
64
70
  const doc = result.body;
65
- const url = formatDocumentUrl(doc);
71
+ const url = formatDocumentUrl(doc, username);
66
72
  await setDocumentMapping(absolutePath, {
67
73
  documentId: doc.id,
68
74
  slug: doc.slug,
@@ -71,32 +77,33 @@ async function updateExisting(api, absolutePath, documentId, content, title, slu
71
77
  });
72
78
  spin.stop("Document updated!");
73
79
  R.success(`Updated: ${url}`);
74
- return;
80
+ return false;
75
81
  }
76
82
  const body = result.body;
77
83
  const errorMsg = body.message ?? body.error ?? "Unknown error";
78
84
  spin.stop(`Update failed: ${errorMsg}`);
79
- if (result.status === 401) {
80
- R.error("Session expired. Run `mdtolink login` to re-authenticate.");
81
- process.exitCode = 1;
82
- return;
83
- }
85
+ if (result.status === 401) return true;
84
86
  if (result.status === 404) {
85
87
  R.warning("Previous document no longer exists on server. Creating a new one...");
86
88
  const newSpin = bt();
87
89
  newSpin.start("Publishing document...");
88
- await publishNew(api, absolutePath, content, title, slug, newSpin);
89
- return;
90
+ await publishNew(api, absolutePath, content, title, slug, newSpin, username);
91
+ return false;
90
92
  }
91
93
  if (result.status === 403) R.info("Upgrade at https://mdtolink.com/pricing");
92
94
  process.exitCode = 1;
95
+ return false;
93
96
  } catch {
94
97
  spin.stop("Update failed.");
95
98
  R.error("Could not connect to server. Check MDTOLINK_SERVER_URL.");
96
99
  process.exitCode = 1;
100
+ return false;
97
101
  }
98
102
  }
99
- async function publishNew(api, absolutePath, content, title, slug, spin) {
103
+ /**
104
+ * Returns true if the caller should prompt re-auth and retry.
105
+ */
106
+ async function publishNew(api, absolutePath, content, title, slug, spin, username) {
100
107
  try {
101
108
  const result = await api.documents.create({ body: {
102
109
  content,
@@ -106,7 +113,7 @@ async function publishNew(api, absolutePath, content, title, slug, spin) {
106
113
  } });
107
114
  if (result.status === 201) {
108
115
  const doc = result.body;
109
- const url = formatDocumentUrl(doc);
116
+ const url = formatDocumentUrl(doc, username);
110
117
  await setDocumentMapping(absolutePath, {
111
118
  documentId: doc.id,
112
119
  slug: doc.slug,
@@ -115,20 +122,22 @@ async function publishNew(api, absolutePath, content, title, slug, spin) {
115
122
  });
116
123
  spin.stop("Published!");
117
124
  R.success(`Published: ${url}`);
118
- return;
125
+ return false;
119
126
  }
120
127
  spin.stop("Publish failed.");
121
128
  const body = result.body;
122
- if (result.status === 401) R.error("Session expired. Run `mdtolink login` to re-authenticate.");
123
- else if (result.status === 403) {
129
+ if (result.status === 401) return true;
130
+ if (result.status === 403) {
124
131
  R.error(body.message ?? body.error);
125
132
  R.info("Upgrade at https://mdtolink.com/pricing");
126
133
  } else R.error(body.message ?? body.error);
127
134
  process.exitCode = 1;
135
+ return false;
128
136
  } catch {
129
137
  spin.stop("Publish failed.");
130
138
  R.error("Could not connect to server. Check MDTOLINK_SERVER_URL.");
131
139
  process.exitCode = 1;
140
+ return false;
132
141
  }
133
142
  }
134
143
 
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { t as createApiClient } from "./api-CPJjLxqz.mjs";
3
+ import { c as getUsername, s as getToken } from "./config-cFx42NU7.mjs";
4
+ import { c as Ct, n as R, r as Re } from "./dist-BbvD4qtQ.mjs";
5
+
6
+ //#region src/cli/lib/require-auth.ts
7
+ /**
8
+ * Ensure the user is authenticated. If no token exists, prompts to login.
9
+ * Returns an auth context with API client, token, and username — or null if auth failed.
10
+ */
11
+ async function requireAuth() {
12
+ let token = await getToken();
13
+ if (!token) {
14
+ const shouldLogin = await Re({ message: "You are not logged in. Would you like to authorize now?" });
15
+ if (Ct(shouldLogin) || !shouldLogin) {
16
+ R.info("Run `mdtolink login` to authenticate.");
17
+ process.exitCode = 1;
18
+ return null;
19
+ }
20
+ const { login } = await import("./login-DOLV04ZV.mjs");
21
+ await login();
22
+ token = await getToken();
23
+ if (!token) {
24
+ R.error("Authentication failed.");
25
+ process.exitCode = 1;
26
+ return null;
27
+ }
28
+ }
29
+ const username = await getUsername();
30
+ return {
31
+ api: createApiClient(token),
32
+ token,
33
+ username
34
+ };
35
+ }
36
+ /**
37
+ * Prompt re-authentication on expired session (401).
38
+ * Returns a fresh auth context, or null if re-auth failed/declined.
39
+ */
40
+ async function promptReauth() {
41
+ const shouldLogin = await Re({ message: "Session expired. Re-authenticate?" });
42
+ if (Ct(shouldLogin) || !shouldLogin) {
43
+ R.info("Run `mdtolink login` to re-authenticate.");
44
+ process.exitCode = 1;
45
+ return null;
46
+ }
47
+ const { login } = await import("./login-DOLV04ZV.mjs");
48
+ await login();
49
+ const token = await getToken();
50
+ if (!token) {
51
+ R.error("Authentication failed.");
52
+ process.exitCode = 1;
53
+ return null;
54
+ }
55
+ const username = await getUsername();
56
+ return {
57
+ api: createApiClient(token),
58
+ token,
59
+ username
60
+ };
61
+ }
62
+
63
+ //#endregion
64
+ export { requireAuth as n, promptReauth as t };
@@ -1,18 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { t as createApiClient } from "./api-D4MGz1n7.mjs";
3
- import { a as getDocumentMap, c as removeDocumentMapping, i as findMappingBySlug, o as getToken } from "./config-B5jPCcGo.mjs";
4
- import { a as bt, i as Ze, n as Re, o as je, s as Ct, t as R } from "./dist-DVBd1E-5.mjs";
2
+ import "./api-CPJjLxqz.mjs";
3
+ import { i as findMappingBySlug, o as getDocumentMap, u as removeDocumentMapping } from "./config-cFx42NU7.mjs";
4
+ import { a as Ze, c as Ct, n as R, o as bt, r as Re, s as je } from "./dist-BbvD4qtQ.mjs";
5
+ import { n as requireAuth } from "./require-auth-m9LOildn.mjs";
5
6
  import { resolve } from "node:path";
6
7
 
7
8
  //#region src/cli/commands/unpublish.ts
8
9
  async function unpublish(fileOrSlug) {
9
- const token = await getToken();
10
- if (!token) {
11
- R.error("Not logged in. Run `mdtolink login` first.");
12
- process.exitCode = 1;
13
- return;
14
- }
15
- const api = createApiClient(token);
10
+ const auth = await requireAuth();
11
+ if (!auth) return;
12
+ const { api } = auth;
16
13
  if (fileOrSlug) await unpublishByArg(api, fileOrSlug);
17
14
  else await unpublishInteractive(api);
18
15
  }
@@ -75,7 +72,7 @@ async function unpublishInteractive(api) {
75
72
  const slug = await Ze({
76
73
  message: "Enter the document slug:",
77
74
  validate: (val) => {
78
- if (val.trim().length === 0) return "Slug cannot be empty.";
75
+ if (!val || val.trim().length === 0) return "Slug cannot be empty.";
79
76
  }
80
77
  });
81
78
  if (Ct(slug)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdtolink",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Publish markdown files to shareable URLs with one command",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -34,17 +34,22 @@
34
34
  "@opentui/core": "^0.1.84",
35
35
  "@ts-rest/core": "3.53.0-rc.1",
36
36
  "@types/node": "22.19.13",
37
+ "@vitest/coverage-v8": "^3.2.3",
37
38
  "better-auth": "^1.4.19",
38
39
  "commander": "^14.0.3",
39
40
  "open": "^10.1.0",
40
41
  "tsdown": "^0.16.8",
41
42
  "tsx": "^4.21.0",
42
43
  "typescript": "5.9.3",
44
+ "vitest": "^3.2.3",
43
45
  "@mdtolink/contracts": "0.0.1"
44
46
  },
45
47
  "scripts": {
46
48
  "build": "tsdown",
47
49
  "dev:tui": "bun run --watch src/index.ts",
48
- "dev:cli": "tsx src/cli/index.ts"
50
+ "dev:cli": "tsx src/cli/index.ts",
51
+ "test": "vitest run",
52
+ "test:watch": "vitest",
53
+ "test:coverage": "vitest run --coverage"
49
54
  }
50
55
  }
File without changes