clipr-cli 0.0.5

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.
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/backends/api.ts
4
+ var ApiBackend = class {
5
+ constructor(config, _shortBaseUrl) {
6
+ this._shortBaseUrl = _shortBaseUrl;
7
+ this.apiBase = config.baseUrl.replace(/\/$/, "");
8
+ this.headers = {
9
+ Authorization: `Bearer ${config.token}`,
10
+ "Content-Type": "application/json"
11
+ };
12
+ }
13
+ apiBase;
14
+ headers;
15
+ /** Make a request and handle errors consistently. */
16
+ async request(method, path, body) {
17
+ const url = `${this.apiBase}${path}`;
18
+ const init = {
19
+ method,
20
+ headers: this.headers
21
+ };
22
+ if (body) {
23
+ init.body = JSON.stringify(body);
24
+ }
25
+ const res = await fetch(url, init);
26
+ if (!res.ok) {
27
+ const text = await res.text().catch(() => "");
28
+ let message = `API error: ${res.status} ${res.statusText}`;
29
+ try {
30
+ const parsed = JSON.parse(text);
31
+ if (parsed.error) message = parsed.error;
32
+ } catch {
33
+ if (text) message += ` \u2014 ${text}`;
34
+ }
35
+ throw new Error(message);
36
+ }
37
+ if (res.status === 204) {
38
+ return void 0;
39
+ }
40
+ return await res.json();
41
+ }
42
+ async create(slug, targetUrl, options) {
43
+ return this.request("POST", "/api/shorten", {
44
+ slug: slug || void 0,
45
+ url: targetUrl,
46
+ ...options
47
+ });
48
+ }
49
+ async resolve(slug) {
50
+ try {
51
+ return await this.request("GET", `/api/links/${encodeURIComponent(slug)}`);
52
+ } catch (err) {
53
+ if (err instanceof Error && err.message.includes("404")) {
54
+ return null;
55
+ }
56
+ throw err;
57
+ }
58
+ }
59
+ async list(options) {
60
+ const params = new URLSearchParams();
61
+ if (options?.search) params.set("search", options.search);
62
+ if (options?.tag) params.set("tag", options.tag);
63
+ if (options?.limit) params.set("limit", String(options.limit));
64
+ const query = params.toString();
65
+ const path = query ? `/api/links?${query}` : "/api/links";
66
+ return this.request("GET", path);
67
+ }
68
+ async delete(slug) {
69
+ await this.request("DELETE", `/api/links/${encodeURIComponent(slug)}`);
70
+ }
71
+ async update(slug, updates) {
72
+ return this.request("PUT", `/api/links/${encodeURIComponent(slug)}`, {
73
+ ...updates
74
+ });
75
+ }
76
+ async getStats(slug) {
77
+ try {
78
+ return await this.request("GET", `/api/stats/${encodeURIComponent(slug)}`);
79
+ } catch (err) {
80
+ if (err instanceof Error && err.message.includes("404")) {
81
+ return null;
82
+ }
83
+ throw err;
84
+ }
85
+ }
86
+ };
87
+ export {
88
+ ApiBackend
89
+ };
90
+ //# sourceMappingURL=api-KFVDRB2Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/backends/api.ts"],"sourcesContent":["import type {\n CreateOptions,\n LinkStats,\n ListOptions,\n ResolveResult,\n ShortUrl,\n UrlBackend,\n} from '@clipr/core';\n\ninterface ApiConfig {\n baseUrl: string;\n token: string;\n}\n\n/**\n * UrlBackend implementation using the clipr Workers API.\n * Each method maps to a REST endpoint on the deployed worker.\n */\nexport class ApiBackend implements UrlBackend {\n private readonly apiBase: string;\n private readonly headers: Record<string, string>;\n\n constructor(\n config: ApiConfig,\n readonly _shortBaseUrl: string,\n ) {\n // Strip trailing slash from API base URL\n this.apiBase = config.baseUrl.replace(/\\/$/, '');\n this.headers = {\n Authorization: `Bearer ${config.token}`,\n 'Content-Type': 'application/json',\n };\n }\n\n /** Make a request and handle errors consistently. */\n private async request<T>(\n method: string,\n path: string,\n body?: Record<string, unknown>,\n ): Promise<T> {\n const url = `${this.apiBase}${path}`;\n const init: RequestInit = {\n method,\n headers: this.headers,\n };\n\n if (body) {\n init.body = JSON.stringify(body);\n }\n\n const res = await fetch(url, init);\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n let message = `API error: ${res.status} ${res.statusText}`;\n try {\n const parsed = JSON.parse(text) as { error?: string };\n if (parsed.error) message = parsed.error;\n } catch {\n if (text) message += ` — ${text}`;\n }\n throw new Error(message);\n }\n\n // 204 No Content\n if (res.status === 204) {\n return undefined as T;\n }\n\n return (await res.json()) as T;\n }\n\n async create(slug: string, targetUrl: string, options?: CreateOptions): Promise<ShortUrl> {\n return this.request<ShortUrl>('POST', '/api/shorten', {\n slug: slug || undefined,\n url: targetUrl,\n ...options,\n });\n }\n\n async resolve(slug: string): Promise<ResolveResult | null> {\n try {\n return await this.request<ResolveResult>('GET', `/api/links/${encodeURIComponent(slug)}`);\n } catch (err) {\n if (err instanceof Error && err.message.includes('404')) {\n return null;\n }\n throw err;\n }\n }\n\n async list(options?: ListOptions): Promise<ShortUrl[]> {\n const params = new URLSearchParams();\n if (options?.search) params.set('search', options.search);\n if (options?.tag) params.set('tag', options.tag);\n if (options?.limit) params.set('limit', String(options.limit));\n\n const query = params.toString();\n const path = query ? `/api/links?${query}` : '/api/links';\n return this.request<ShortUrl[]>('GET', path);\n }\n\n async delete(slug: string): Promise<void> {\n await this.request<void>('DELETE', `/api/links/${encodeURIComponent(slug)}`);\n }\n\n async update(slug: string, updates: Partial<ShortUrl>): Promise<ShortUrl> {\n return this.request<ShortUrl>('PUT', `/api/links/${encodeURIComponent(slug)}`, {\n ...(updates as Record<string, unknown>),\n });\n }\n\n async getStats(slug: string): Promise<LinkStats | null> {\n try {\n return await this.request<LinkStats>('GET', `/api/stats/${encodeURIComponent(slug)}`);\n } catch (err) {\n if (err instanceof Error && err.message.includes('404')) {\n return null;\n }\n throw err;\n }\n }\n}\n"],"mappings":";;;AAkBO,IAAM,aAAN,MAAuC;AAAA,EAI5C,YACE,QACS,eACT;AADS;AAGT,SAAK,UAAU,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAC/C,SAAK,UAAU;AAAA,MACb,eAAe,UAAU,OAAO,KAAK;AAAA,MACrC,gBAAgB;AAAA,IAClB;AAAA,EACF;AAAA,EAbiB;AAAA,EACA;AAAA;AAAA,EAejB,MAAc,QACZ,QACA,MACA,MACY;AACZ,UAAM,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI;AAClC,UAAM,OAAoB;AAAA,MACxB;AAAA,MACA,SAAS,KAAK;AAAA,IAChB;AAEA,QAAI,MAAM;AACR,WAAK,OAAO,KAAK,UAAU,IAAI;AAAA,IACjC;AAEA,UAAM,MAAM,MAAM,MAAM,KAAK,IAAI;AAEjC,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,UAAI,UAAU,cAAc,IAAI,MAAM,IAAI,IAAI,UAAU;AACxD,UAAI;AACF,cAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,YAAI,OAAO,MAAO,WAAU,OAAO;AAAA,MACrC,QAAQ;AACN,YAAI,KAAM,YAAW,WAAM,IAAI;AAAA,MACjC;AACA,YAAM,IAAI,MAAM,OAAO;AAAA,IACzB;AAGA,QAAI,IAAI,WAAW,KAAK;AACtB,aAAO;AAAA,IACT;AAEA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAAA,EAEA,MAAM,OAAO,MAAc,WAAmB,SAA4C;AACxF,WAAO,KAAK,QAAkB,QAAQ,gBAAgB;AAAA,MACpD,MAAM,QAAQ;AAAA,MACd,KAAK;AAAA,MACL,GAAG;AAAA,IACL,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAQ,MAA6C;AACzD,QAAI;AACF,aAAO,MAAM,KAAK,QAAuB,OAAO,cAAc,mBAAmB,IAAI,CAAC,EAAE;AAAA,IAC1F,SAAS,KAAK;AACZ,UAAI,eAAe,SAAS,IAAI,QAAQ,SAAS,KAAK,GAAG;AACvD,eAAO;AAAA,MACT;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,SAA4C;AACrD,UAAM,SAAS,IAAI,gBAAgB;AACnC,QAAI,SAAS,OAAQ,QAAO,IAAI,UAAU,QAAQ,MAAM;AACxD,QAAI,SAAS,IAAK,QAAO,IAAI,OAAO,QAAQ,GAAG;AAC/C,QAAI,SAAS,MAAO,QAAO,IAAI,SAAS,OAAO,QAAQ,KAAK,CAAC;AAE7D,UAAM,QAAQ,OAAO,SAAS;AAC9B,UAAM,OAAO,QAAQ,cAAc,KAAK,KAAK;AAC7C,WAAO,KAAK,QAAoB,OAAO,IAAI;AAAA,EAC7C;AAAA,EAEA,MAAM,OAAO,MAA6B;AACxC,UAAM,KAAK,QAAc,UAAU,cAAc,mBAAmB,IAAI,CAAC,EAAE;AAAA,EAC7E;AAAA,EAEA,MAAM,OAAO,MAAc,SAA+C;AACxE,WAAO,KAAK,QAAkB,OAAO,cAAc,mBAAmB,IAAI,CAAC,IAAI;AAAA,MAC7E,GAAI;AAAA,IACN,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,SAAS,MAAyC;AACtD,QAAI;AACF,aAAO,MAAM,KAAK,QAAmB,OAAO,cAAc,mBAAmB,IAAI,CAAC,EAAE;AAAA,IACtF,SAAS,KAAK;AACZ,UAAI,eAAe,SAAS,IAAI,QAAQ,SAAS,KAAK,GAAG;AACvD,eAAO;AAAA,MACT;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,88 @@
1
+ // src/backends/api.ts
2
+ var ApiBackend = class {
3
+ constructor(config, _shortBaseUrl) {
4
+ this._shortBaseUrl = _shortBaseUrl;
5
+ this.apiBase = config.baseUrl.replace(/\/$/, "");
6
+ this.headers = {
7
+ Authorization: `Bearer ${config.token}`,
8
+ "Content-Type": "application/json"
9
+ };
10
+ }
11
+ apiBase;
12
+ headers;
13
+ /** Make a request and handle errors consistently. */
14
+ async request(method, path, body) {
15
+ const url = `${this.apiBase}${path}`;
16
+ const init = {
17
+ method,
18
+ headers: this.headers
19
+ };
20
+ if (body) {
21
+ init.body = JSON.stringify(body);
22
+ }
23
+ const res = await fetch(url, init);
24
+ if (!res.ok) {
25
+ const text = await res.text().catch(() => "");
26
+ let message = `API error: ${res.status} ${res.statusText}`;
27
+ try {
28
+ const parsed = JSON.parse(text);
29
+ if (parsed.error) message = parsed.error;
30
+ } catch {
31
+ if (text) message += ` \u2014 ${text}`;
32
+ }
33
+ throw new Error(message);
34
+ }
35
+ if (res.status === 204) {
36
+ return void 0;
37
+ }
38
+ return await res.json();
39
+ }
40
+ async create(slug, targetUrl, options) {
41
+ return this.request("POST", "/api/shorten", {
42
+ slug: slug || void 0,
43
+ url: targetUrl,
44
+ ...options
45
+ });
46
+ }
47
+ async resolve(slug) {
48
+ try {
49
+ return await this.request("GET", `/api/links/${encodeURIComponent(slug)}`);
50
+ } catch (err) {
51
+ if (err instanceof Error && err.message.includes("404")) {
52
+ return null;
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+ async list(options) {
58
+ const params = new URLSearchParams();
59
+ if (options?.search) params.set("search", options.search);
60
+ if (options?.tag) params.set("tag", options.tag);
61
+ if (options?.limit) params.set("limit", String(options.limit));
62
+ const query = params.toString();
63
+ const path = query ? `/api/links?${query}` : "/api/links";
64
+ return this.request("GET", path);
65
+ }
66
+ async delete(slug) {
67
+ await this.request("DELETE", `/api/links/${encodeURIComponent(slug)}`);
68
+ }
69
+ async update(slug, updates) {
70
+ return this.request("PUT", `/api/links/${encodeURIComponent(slug)}`, {
71
+ ...updates
72
+ });
73
+ }
74
+ async getStats(slug) {
75
+ try {
76
+ return await this.request("GET", `/api/stats/${encodeURIComponent(slug)}`);
77
+ } catch (err) {
78
+ if (err instanceof Error && err.message.includes("404")) {
79
+ return null;
80
+ }
81
+ throw err;
82
+ }
83
+ }
84
+ };
85
+ export {
86
+ ApiBackend
87
+ };
88
+ //# sourceMappingURL=api-LBSFNXNQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/backends/api.ts"],"sourcesContent":["import type {\n CreateOptions,\n LinkStats,\n ListOptions,\n ResolveResult,\n ShortUrl,\n UrlBackend,\n} from '@clipr/core';\n\ninterface ApiConfig {\n baseUrl: string;\n token: string;\n}\n\n/**\n * UrlBackend implementation using the clipr Workers API.\n * Each method maps to a REST endpoint on the deployed worker.\n */\nexport class ApiBackend implements UrlBackend {\n private readonly apiBase: string;\n private readonly headers: Record<string, string>;\n\n constructor(\n config: ApiConfig,\n readonly _shortBaseUrl: string,\n ) {\n // Strip trailing slash from API base URL\n this.apiBase = config.baseUrl.replace(/\\/$/, '');\n this.headers = {\n Authorization: `Bearer ${config.token}`,\n 'Content-Type': 'application/json',\n };\n }\n\n /** Make a request and handle errors consistently. */\n private async request<T>(\n method: string,\n path: string,\n body?: Record<string, unknown>,\n ): Promise<T> {\n const url = `${this.apiBase}${path}`;\n const init: RequestInit = {\n method,\n headers: this.headers,\n };\n\n if (body) {\n init.body = JSON.stringify(body);\n }\n\n const res = await fetch(url, init);\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n let message = `API error: ${res.status} ${res.statusText}`;\n try {\n const parsed = JSON.parse(text) as { error?: string };\n if (parsed.error) message = parsed.error;\n } catch {\n if (text) message += ` — ${text}`;\n }\n throw new Error(message);\n }\n\n // 204 No Content\n if (res.status === 204) {\n return undefined as T;\n }\n\n return (await res.json()) as T;\n }\n\n async create(slug: string, targetUrl: string, options?: CreateOptions): Promise<ShortUrl> {\n return this.request<ShortUrl>('POST', '/api/shorten', {\n slug: slug || undefined,\n url: targetUrl,\n ...options,\n });\n }\n\n async resolve(slug: string): Promise<ResolveResult | null> {\n try {\n return await this.request<ResolveResult>('GET', `/api/links/${encodeURIComponent(slug)}`);\n } catch (err) {\n if (err instanceof Error && err.message.includes('404')) {\n return null;\n }\n throw err;\n }\n }\n\n async list(options?: ListOptions): Promise<ShortUrl[]> {\n const params = new URLSearchParams();\n if (options?.search) params.set('search', options.search);\n if (options?.tag) params.set('tag', options.tag);\n if (options?.limit) params.set('limit', String(options.limit));\n\n const query = params.toString();\n const path = query ? `/api/links?${query}` : '/api/links';\n return this.request<ShortUrl[]>('GET', path);\n }\n\n async delete(slug: string): Promise<void> {\n await this.request<void>('DELETE', `/api/links/${encodeURIComponent(slug)}`);\n }\n\n async update(slug: string, updates: Partial<ShortUrl>): Promise<ShortUrl> {\n return this.request<ShortUrl>('PUT', `/api/links/${encodeURIComponent(slug)}`, {\n ...(updates as Record<string, unknown>),\n });\n }\n\n async getStats(slug: string): Promise<LinkStats | null> {\n try {\n return await this.request<LinkStats>('GET', `/api/stats/${encodeURIComponent(slug)}`);\n } catch (err) {\n if (err instanceof Error && err.message.includes('404')) {\n return null;\n }\n throw err;\n }\n }\n}\n"],"mappings":";AAkBO,IAAM,aAAN,MAAuC;AAAA,EAI5C,YACE,QACS,eACT;AADS;AAGT,SAAK,UAAU,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAC/C,SAAK,UAAU;AAAA,MACb,eAAe,UAAU,OAAO,KAAK;AAAA,MACrC,gBAAgB;AAAA,IAClB;AAAA,EACF;AAAA,EAbiB;AAAA,EACA;AAAA;AAAA,EAejB,MAAc,QACZ,QACA,MACA,MACY;AACZ,UAAM,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI;AAClC,UAAM,OAAoB;AAAA,MACxB;AAAA,MACA,SAAS,KAAK;AAAA,IAChB;AAEA,QAAI,MAAM;AACR,WAAK,OAAO,KAAK,UAAU,IAAI;AAAA,IACjC;AAEA,UAAM,MAAM,MAAM,MAAM,KAAK,IAAI;AAEjC,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,UAAI,UAAU,cAAc,IAAI,MAAM,IAAI,IAAI,UAAU;AACxD,UAAI;AACF,cAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,YAAI,OAAO,MAAO,WAAU,OAAO;AAAA,MACrC,QAAQ;AACN,YAAI,KAAM,YAAW,WAAM,IAAI;AAAA,MACjC;AACA,YAAM,IAAI,MAAM,OAAO;AAAA,IACzB;AAGA,QAAI,IAAI,WAAW,KAAK;AACtB,aAAO;AAAA,IACT;AAEA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAAA,EAEA,MAAM,OAAO,MAAc,WAAmB,SAA4C;AACxF,WAAO,KAAK,QAAkB,QAAQ,gBAAgB;AAAA,MACpD,MAAM,QAAQ;AAAA,MACd,KAAK;AAAA,MACL,GAAG;AAAA,IACL,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAQ,MAA6C;AACzD,QAAI;AACF,aAAO,MAAM,KAAK,QAAuB,OAAO,cAAc,mBAAmB,IAAI,CAAC,EAAE;AAAA,IAC1F,SAAS,KAAK;AACZ,UAAI,eAAe,SAAS,IAAI,QAAQ,SAAS,KAAK,GAAG;AACvD,eAAO;AAAA,MACT;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,SAA4C;AACrD,UAAM,SAAS,IAAI,gBAAgB;AACnC,QAAI,SAAS,OAAQ,QAAO,IAAI,UAAU,QAAQ,MAAM;AACxD,QAAI,SAAS,IAAK,QAAO,IAAI,OAAO,QAAQ,GAAG;AAC/C,QAAI,SAAS,MAAO,QAAO,IAAI,SAAS,OAAO,QAAQ,KAAK,CAAC;AAE7D,UAAM,QAAQ,OAAO,SAAS;AAC9B,UAAM,OAAO,QAAQ,cAAc,KAAK,KAAK;AAC7C,WAAO,KAAK,QAAoB,OAAO,IAAI;AAAA,EAC7C;AAAA,EAEA,MAAM,OAAO,MAA6B;AACxC,UAAM,KAAK,QAAc,UAAU,cAAc,mBAAmB,IAAI,CAAC,EAAE;AAAA,EAC7E;AAAA,EAEA,MAAM,OAAO,MAAc,SAA+C;AACxE,WAAO,KAAK,QAAkB,OAAO,cAAc,mBAAmB,IAAI,CAAC,IAAI;AAAA,MAC7E,GAAI;AAAA,IACN,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,SAAS,MAAyC;AACtD,QAAI;AACF,aAAO,MAAM,KAAK,QAAmB,OAAO,cAAc,mBAAmB,IAAI,CAAC,EAAE;AAAA,IACtF,SAAS,KAAK;AACZ,UAAI,eAAe,SAAS,IAAI,QAAQ,SAAS,KAAK,GAAG;AACvD,eAAO;AAAA,MACT;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;","names":[]}
package/dist/api.d.ts ADDED
@@ -0,0 +1,86 @@
1
+ import { CliprConfig, ListOptions, ShortUrl, ResolveResult, CreateOptions, LinkStats } from '@clipr/core';
2
+
3
+ /**
4
+ * Create a short URL.
5
+ *
6
+ * @param url - The target URL to shorten.
7
+ * @param options - Optional creation settings (slug, tags, expiry, etc.).
8
+ * @returns The created short URL entry.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { shorten } from 'clipr';
13
+ * const link = await shorten('https://example.com', { slug: 'demo' });
14
+ * ```
15
+ */
16
+ declare function shorten(url: string, options?: CreateOptions): Promise<ShortUrl>;
17
+ /**
18
+ * List short URLs with optional filtering.
19
+ *
20
+ * @param options - Optional filter/search/limit settings.
21
+ * @returns Array of short URL entries.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { list } from 'clipr';
26
+ * const links = await list({ limit: 10 });
27
+ * ```
28
+ */
29
+ declare function list(options?: ListOptions): Promise<ShortUrl[]>;
30
+ /**
31
+ * Resolve a slug to its target URL.
32
+ *
33
+ * @param slug - The short slug to resolve.
34
+ * @returns The resolve result with target URL, or null if not found.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { resolve } from 'clipr';
39
+ * const result = await resolve('demo');
40
+ * if (result) console.log(result.url);
41
+ * ```
42
+ */
43
+ declare function resolve(slug: string): Promise<ResolveResult | null>;
44
+ /**
45
+ * Delete a short URL by slug.
46
+ *
47
+ * @param slug - The slug to delete.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { remove } from 'clipr';
52
+ * await remove('demo');
53
+ * ```
54
+ */
55
+ declare function remove(slug: string): Promise<void>;
56
+ /**
57
+ * Get click analytics for a slug.
58
+ * Returns null if the backend does not support stats (e.g., json/github mode).
59
+ *
60
+ * @param slug - The slug to get stats for.
61
+ * @returns Click analytics or null.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { stats } from 'clipr';
66
+ * const data = await stats('demo');
67
+ * if (data) console.log(`Total clicks: ${data.total}`);
68
+ * ```
69
+ */
70
+ declare function stats(slug: string): Promise<LinkStats | null>;
71
+ /**
72
+ * Configure the clipr backend.
73
+ * Merges the provided config with the existing config and persists it.
74
+ * Resets the backend singleton so subsequent calls use the new config.
75
+ *
76
+ * @param config - Partial config to merge.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * import { configure } from 'clipr';
81
+ * configure({ mode: 'api', api: { baseUrl: 'https://clpr.sh', token: 'xxx' } });
82
+ * ```
83
+ */
84
+ declare function configure(config: Partial<CliprConfig>): void;
85
+
86
+ export { configure, list, remove, resolve, shorten, stats };
package/dist/api.js ADDED
@@ -0,0 +1,165 @@
1
+ // src/api.ts
2
+ import {
3
+ loadConfig,
4
+ saveConfig
5
+ } from "@clipr/core";
6
+
7
+ // src/backends/json-adapter.ts
8
+ import {
9
+ appendUtm,
10
+ generateRandomSlug,
11
+ hasUtm,
12
+ JsonBackend,
13
+ normalizeSlug,
14
+ SlugNotFoundError
15
+ } from "@clipr/core";
16
+ var JsonBackendAdapter = class {
17
+ constructor(dbPath, _baseUrl) {
18
+ this._baseUrl = _baseUrl;
19
+ this.backend = new JsonBackend(dbPath);
20
+ }
21
+ backend;
22
+ async create(slug, targetUrl, options) {
23
+ const finalSlug = slug || generateRandomSlug(options?.utm ? 8 : 6);
24
+ const now = (/* @__PURE__ */ new Date()).toISOString();
25
+ const entry = {
26
+ slug: normalizeSlug(finalSlug),
27
+ url: targetUrl,
28
+ createdAt: now,
29
+ ...options?.description && { description: options.description },
30
+ ...options?.expiresAt && { expiresAt: options.expiresAt },
31
+ ...options?.utm && hasUtm(options.utm) && { utm: options.utm },
32
+ ...options?.tags && { tags: options.tags },
33
+ ...options?.ogTitle && { ogTitle: options.ogTitle },
34
+ ...options?.ogDescription && { ogDescription: options.ogDescription },
35
+ ...options?.ogImage && { ogImage: options.ogImage }
36
+ };
37
+ await this.backend.set(entry);
38
+ return entry;
39
+ }
40
+ async resolve(slug) {
41
+ const entry = await this.backend.get(slug);
42
+ if (!entry) return null;
43
+ const expired = entry.expiresAt ? new Date(entry.expiresAt) < /* @__PURE__ */ new Date() : false;
44
+ const url = hasUtm(entry.utm) ? appendUtm(entry.url, entry.utm) : entry.url;
45
+ return {
46
+ url,
47
+ passwordProtected: false,
48
+ expired
49
+ };
50
+ }
51
+ async list(options) {
52
+ let entries = await this.backend.list();
53
+ if (options?.search) {
54
+ const q = options.search.toLowerCase();
55
+ entries = entries.filter(
56
+ (e) => e.slug.includes(q) || e.url.toLowerCase().includes(q) || e.description?.toLowerCase().includes(q)
57
+ );
58
+ }
59
+ if (options?.tag) {
60
+ entries = entries.filter((e) => e.tags?.includes(options.tag));
61
+ }
62
+ if (options?.limit) {
63
+ entries = entries.slice(0, options.limit);
64
+ }
65
+ return entries;
66
+ }
67
+ async delete(slug) {
68
+ const deleted = await this.backend.delete(slug);
69
+ if (!deleted) {
70
+ throw new SlugNotFoundError(slug);
71
+ }
72
+ }
73
+ async update(slug, updates) {
74
+ const existing = await this.backend.get(slug);
75
+ if (!existing) {
76
+ throw new SlugNotFoundError(slug);
77
+ }
78
+ const newSlug = updates.slug ?? slug;
79
+ if (newSlug !== slug) {
80
+ await this.backend.delete(slug);
81
+ } else {
82
+ await this.backend.delete(slug);
83
+ }
84
+ const updated = { ...existing, ...updates, slug: newSlug };
85
+ await this.backend.set(updated);
86
+ return updated;
87
+ }
88
+ async getStats(_slug) {
89
+ return null;
90
+ }
91
+ };
92
+
93
+ // src/factory.ts
94
+ async function createBackend(config) {
95
+ switch (config.mode) {
96
+ case "github": {
97
+ if (!config.github) {
98
+ throw new Error(
99
+ "GitHub backend requires github config. Run `clipr config` to set owner, repo, branch, path, and token."
100
+ );
101
+ }
102
+ const { GitHubBackend } = await import("./github-OYIAPRKM.js");
103
+ return new GitHubBackend(config.github, config.baseUrl);
104
+ }
105
+ case "api": {
106
+ if (!config.api) {
107
+ throw new Error(
108
+ "API backend requires api config. Run `clipr config` to set baseUrl and token."
109
+ );
110
+ }
111
+ const { ApiBackend } = await import("./api-LBSFNXNQ.js");
112
+ return new ApiBackend(config.api, config.baseUrl);
113
+ }
114
+ default: {
115
+ return new JsonBackendAdapter(config.dbPath, config.baseUrl);
116
+ }
117
+ }
118
+ }
119
+
120
+ // src/api.ts
121
+ var _backend = null;
122
+ var _config = null;
123
+ async function getBackend() {
124
+ if (!_backend) {
125
+ _config = _config ?? loadConfig();
126
+ _backend = await createBackend(_config);
127
+ }
128
+ return _backend;
129
+ }
130
+ async function shorten(url, options) {
131
+ const backend = await getBackend();
132
+ return backend.create(options?.slug ?? "", url, options);
133
+ }
134
+ async function list(options) {
135
+ const backend = await getBackend();
136
+ return backend.list(options);
137
+ }
138
+ async function resolve(slug) {
139
+ const backend = await getBackend();
140
+ return backend.resolve(slug);
141
+ }
142
+ async function remove(slug) {
143
+ const backend = await getBackend();
144
+ return backend.delete(slug);
145
+ }
146
+ async function stats(slug) {
147
+ const backend = await getBackend();
148
+ return backend.getStats(slug);
149
+ }
150
+ function configure(config) {
151
+ const current = _config ?? loadConfig();
152
+ const merged = { ...current, ...config };
153
+ saveConfig(merged);
154
+ _config = merged;
155
+ _backend = null;
156
+ }
157
+ export {
158
+ configure,
159
+ list,
160
+ remove,
161
+ resolve,
162
+ shorten,
163
+ stats
164
+ };
165
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api.ts","../src/backends/json-adapter.ts","../src/factory.ts"],"sourcesContent":["import {\n type CliprConfig,\n type CreateOptions,\n type LinkStats,\n type ListOptions,\n loadConfig,\n type ResolveResult,\n type ShortUrl,\n saveConfig,\n type UrlBackend,\n} from '@clipr/core';\nimport { createBackend } from './factory.js';\n\nlet _backend: UrlBackend | null = null;\nlet _config: CliprConfig | null = null;\n\n/** Get or create the backend singleton. */\nasync function getBackend(): Promise<UrlBackend> {\n if (!_backend) {\n _config = _config ?? loadConfig();\n _backend = await createBackend(_config);\n }\n return _backend;\n}\n\n/**\n * Create a short URL.\n *\n * @param url - The target URL to shorten.\n * @param options - Optional creation settings (slug, tags, expiry, etc.).\n * @returns The created short URL entry.\n *\n * @example\n * ```ts\n * import { shorten } from 'clipr';\n * const link = await shorten('https://example.com', { slug: 'demo' });\n * ```\n */\nexport async function shorten(url: string, options?: CreateOptions): Promise<ShortUrl> {\n const backend = await getBackend();\n return backend.create(options?.slug ?? '', url, options);\n}\n\n/**\n * List short URLs with optional filtering.\n *\n * @param options - Optional filter/search/limit settings.\n * @returns Array of short URL entries.\n *\n * @example\n * ```ts\n * import { list } from 'clipr';\n * const links = await list({ limit: 10 });\n * ```\n */\nexport async function list(options?: ListOptions): Promise<ShortUrl[]> {\n const backend = await getBackend();\n return backend.list(options);\n}\n\n/**\n * Resolve a slug to its target URL.\n *\n * @param slug - The short slug to resolve.\n * @returns The resolve result with target URL, or null if not found.\n *\n * @example\n * ```ts\n * import { resolve } from 'clipr';\n * const result = await resolve('demo');\n * if (result) console.log(result.url);\n * ```\n */\nexport async function resolve(slug: string): Promise<ResolveResult | null> {\n const backend = await getBackend();\n return backend.resolve(slug);\n}\n\n/**\n * Delete a short URL by slug.\n *\n * @param slug - The slug to delete.\n *\n * @example\n * ```ts\n * import { remove } from 'clipr';\n * await remove('demo');\n * ```\n */\nexport async function remove(slug: string): Promise<void> {\n const backend = await getBackend();\n return backend.delete(slug);\n}\n\n/**\n * Get click analytics for a slug.\n * Returns null if the backend does not support stats (e.g., json/github mode).\n *\n * @param slug - The slug to get stats for.\n * @returns Click analytics or null.\n *\n * @example\n * ```ts\n * import { stats } from 'clipr';\n * const data = await stats('demo');\n * if (data) console.log(`Total clicks: ${data.total}`);\n * ```\n */\nexport async function stats(slug: string): Promise<LinkStats | null> {\n const backend = await getBackend();\n return backend.getStats(slug);\n}\n\n/**\n * Configure the clipr backend.\n * Merges the provided config with the existing config and persists it.\n * Resets the backend singleton so subsequent calls use the new config.\n *\n * @param config - Partial config to merge.\n *\n * @example\n * ```ts\n * import { configure } from 'clipr';\n * configure({ mode: 'api', api: { baseUrl: 'https://clpr.sh', token: 'xxx' } });\n * ```\n */\nexport function configure(config: Partial<CliprConfig>): void {\n const current = _config ?? loadConfig();\n const merged = { ...current, ...config } as CliprConfig;\n saveConfig(merged);\n _config = merged;\n // Reset backend so next call picks up new config\n _backend = null;\n}\n","import {\n appendUtm,\n type CreateOptions,\n generateRandomSlug,\n hasUtm,\n JsonBackend,\n type LinkStats,\n type ListOptions,\n normalizeSlug,\n type ResolveResult,\n type ShortUrl,\n SlugNotFoundError,\n type UrlBackend,\n} from '@clipr/core';\n\n/**\n * Wraps the simple JsonBackend to satisfy the full UrlBackend interface.\n * Used as the default/fallback when no remote backend is configured.\n */\nexport class JsonBackendAdapter implements UrlBackend {\n private readonly backend: JsonBackend;\n\n constructor(\n dbPath: string,\n readonly _baseUrl: string,\n ) {\n this.backend = new JsonBackend(dbPath);\n }\n\n async create(slug: string, targetUrl: string, options?: CreateOptions): Promise<ShortUrl> {\n const finalSlug = slug || generateRandomSlug(options?.utm ? 8 : 6);\n const now = new Date().toISOString();\n\n const entry: ShortUrl = {\n slug: normalizeSlug(finalSlug),\n url: targetUrl,\n createdAt: now,\n ...(options?.description && { description: options.description }),\n ...(options?.expiresAt && { expiresAt: options.expiresAt }),\n ...(options?.utm && hasUtm(options.utm) && { utm: options.utm }),\n ...(options?.tags && { tags: options.tags }),\n ...(options?.ogTitle && { ogTitle: options.ogTitle }),\n ...(options?.ogDescription && { ogDescription: options.ogDescription }),\n ...(options?.ogImage && { ogImage: options.ogImage }),\n };\n\n await this.backend.set(entry);\n return entry;\n }\n\n async resolve(slug: string): Promise<ResolveResult | null> {\n const entry = await this.backend.get(slug);\n if (!entry) return null;\n\n const expired = entry.expiresAt ? new Date(entry.expiresAt) < new Date() : false;\n const url = hasUtm(entry.utm) ? appendUtm(entry.url, entry.utm!) : entry.url;\n\n return {\n url,\n passwordProtected: false,\n expired,\n };\n }\n\n async list(options?: ListOptions): Promise<ShortUrl[]> {\n let entries = (await this.backend.list()) as ShortUrl[];\n\n if (options?.search) {\n const q = options.search.toLowerCase();\n entries = entries.filter(\n (e) =>\n e.slug.includes(q) ||\n e.url.toLowerCase().includes(q) ||\n e.description?.toLowerCase().includes(q),\n );\n }\n\n if (options?.tag) {\n entries = entries.filter((e) => e.tags?.includes(options.tag!));\n }\n\n if (options?.limit) {\n entries = entries.slice(0, options.limit);\n }\n\n return entries;\n }\n\n async delete(slug: string): Promise<void> {\n const deleted = await this.backend.delete(slug);\n if (!deleted) {\n throw new SlugNotFoundError(slug);\n }\n }\n\n async update(slug: string, updates: Partial<ShortUrl>): Promise<ShortUrl> {\n const existing = await this.backend.get(slug);\n if (!existing) {\n throw new SlugNotFoundError(slug);\n }\n\n // If slug is changing, delete old and create new\n const newSlug = updates.slug ?? slug;\n if (newSlug !== slug) {\n await this.backend.delete(slug);\n } else {\n // Delete old to allow re-set\n await this.backend.delete(slug);\n }\n\n const updated: ShortUrl = { ...existing, ...updates, slug: newSlug };\n await this.backend.set(updated);\n return updated;\n }\n\n async getStats(_slug: string): Promise<LinkStats | null> {\n // JsonBackend has no click tracking\n return null;\n }\n}\n","import type { CliprConfig, UrlBackend } from '@clipr/core';\n\nimport { JsonBackendAdapter } from './backends/json-adapter.js';\n\n/**\n * Factory function (composition root).\n * Creates the appropriate UrlBackend based on the config mode.\n * Uses dynamic imports so backend modules are only loaded when needed.\n */\nexport async function createBackend(config: CliprConfig): Promise<UrlBackend> {\n switch (config.mode) {\n case 'github': {\n if (!config.github) {\n throw new Error(\n 'GitHub backend requires github config. Run `clipr config` to set owner, repo, branch, path, and token.',\n );\n }\n const { GitHubBackend } = await import('./backends/github.js');\n return new GitHubBackend(config.github, config.baseUrl);\n }\n\n case 'api': {\n if (!config.api) {\n throw new Error(\n 'API backend requires api config. Run `clipr config` to set baseUrl and token.',\n );\n }\n const { ApiBackend } = await import('./backends/api.js');\n return new ApiBackend(config.api, config.baseUrl);\n }\n\n default: {\n // Fall back to local JsonBackend wrapped as UrlBackend\n return new JsonBackendAdapter(config.dbPath, config.baseUrl);\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/backends/json-adapter.ts
4
+ import {
5
+ appendUtm,
6
+ generateRandomSlug,
7
+ hasUtm,
8
+ JsonBackend,
9
+ normalizeSlug,
10
+ SlugNotFoundError
11
+ } from "@clipr/core";
12
+ var JsonBackendAdapter = class {
13
+ constructor(dbPath, _baseUrl) {
14
+ this._baseUrl = _baseUrl;
15
+ this.backend = new JsonBackend(dbPath);
16
+ }
17
+ backend;
18
+ async create(slug, targetUrl, options) {
19
+ const finalSlug = slug || generateRandomSlug(options?.utm ? 8 : 6);
20
+ const now = (/* @__PURE__ */ new Date()).toISOString();
21
+ const entry = {
22
+ slug: normalizeSlug(finalSlug),
23
+ url: targetUrl,
24
+ createdAt: now,
25
+ ...options?.description && { description: options.description },
26
+ ...options?.expiresAt && { expiresAt: options.expiresAt },
27
+ ...options?.utm && hasUtm(options.utm) && { utm: options.utm },
28
+ ...options?.tags && { tags: options.tags },
29
+ ...options?.ogTitle && { ogTitle: options.ogTitle },
30
+ ...options?.ogDescription && { ogDescription: options.ogDescription },
31
+ ...options?.ogImage && { ogImage: options.ogImage }
32
+ };
33
+ await this.backend.set(entry);
34
+ return entry;
35
+ }
36
+ async resolve(slug) {
37
+ const entry = await this.backend.get(slug);
38
+ if (!entry) return null;
39
+ const expired = entry.expiresAt ? new Date(entry.expiresAt) < /* @__PURE__ */ new Date() : false;
40
+ const url = hasUtm(entry.utm) ? appendUtm(entry.url, entry.utm) : entry.url;
41
+ return {
42
+ url,
43
+ passwordProtected: false,
44
+ expired
45
+ };
46
+ }
47
+ async list(options) {
48
+ let entries = await this.backend.list();
49
+ if (options?.search) {
50
+ const q = options.search.toLowerCase();
51
+ entries = entries.filter(
52
+ (e) => e.slug.includes(q) || e.url.toLowerCase().includes(q) || e.description?.toLowerCase().includes(q)
53
+ );
54
+ }
55
+ if (options?.tag) {
56
+ entries = entries.filter((e) => e.tags?.includes(options.tag));
57
+ }
58
+ if (options?.limit) {
59
+ entries = entries.slice(0, options.limit);
60
+ }
61
+ return entries;
62
+ }
63
+ async delete(slug) {
64
+ const deleted = await this.backend.delete(slug);
65
+ if (!deleted) {
66
+ throw new SlugNotFoundError(slug);
67
+ }
68
+ }
69
+ async update(slug, updates) {
70
+ const existing = await this.backend.get(slug);
71
+ if (!existing) {
72
+ throw new SlugNotFoundError(slug);
73
+ }
74
+ const newSlug = updates.slug ?? slug;
75
+ if (newSlug !== slug) {
76
+ await this.backend.delete(slug);
77
+ } else {
78
+ await this.backend.delete(slug);
79
+ }
80
+ const updated = { ...existing, ...updates, slug: newSlug };
81
+ await this.backend.set(updated);
82
+ return updated;
83
+ }
84
+ async getStats(_slug) {
85
+ return null;
86
+ }
87
+ };
88
+
89
+ export {
90
+ JsonBackendAdapter
91
+ };
92
+ //# sourceMappingURL=chunk-I7CHG5Z3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/backends/json-adapter.ts"],"sourcesContent":["import {\n appendUtm,\n type CreateOptions,\n generateRandomSlug,\n hasUtm,\n JsonBackend,\n type LinkStats,\n type ListOptions,\n normalizeSlug,\n type ResolveResult,\n type ShortUrl,\n SlugNotFoundError,\n type UrlBackend,\n} from '@clipr/core';\n\n/**\n * Wraps the simple JsonBackend to satisfy the full UrlBackend interface.\n * Used as the default/fallback when no remote backend is configured.\n */\nexport class JsonBackendAdapter implements UrlBackend {\n private readonly backend: JsonBackend;\n\n constructor(\n dbPath: string,\n readonly _baseUrl: string,\n ) {\n this.backend = new JsonBackend(dbPath);\n }\n\n async create(slug: string, targetUrl: string, options?: CreateOptions): Promise<ShortUrl> {\n const finalSlug = slug || generateRandomSlug(options?.utm ? 8 : 6);\n const now = new Date().toISOString();\n\n const entry: ShortUrl = {\n slug: normalizeSlug(finalSlug),\n url: targetUrl,\n createdAt: now,\n ...(options?.description && { description: options.description }),\n ...(options?.expiresAt && { expiresAt: options.expiresAt }),\n ...(options?.utm && hasUtm(options.utm) && { utm: options.utm }),\n ...(options?.tags && { tags: options.tags }),\n ...(options?.ogTitle && { ogTitle: options.ogTitle }),\n ...(options?.ogDescription && { ogDescription: options.ogDescription }),\n ...(options?.ogImage && { ogImage: options.ogImage }),\n };\n\n await this.backend.set(entry);\n return entry;\n }\n\n async resolve(slug: string): Promise<ResolveResult | null> {\n const entry = await this.backend.get(slug);\n if (!entry) return null;\n\n const expired = entry.expiresAt ? new Date(entry.expiresAt) < new Date() : false;\n const url = hasUtm(entry.utm) ? appendUtm(entry.url, entry.utm!) : entry.url;\n\n return {\n url,\n passwordProtected: false,\n expired,\n };\n }\n\n async list(options?: ListOptions): Promise<ShortUrl[]> {\n let entries = (await this.backend.list()) as ShortUrl[];\n\n if (options?.search) {\n const q = options.search.toLowerCase();\n entries = entries.filter(\n (e) =>\n e.slug.includes(q) ||\n e.url.toLowerCase().includes(q) ||\n e.description?.toLowerCase().includes(q),\n );\n }\n\n if (options?.tag) {\n entries = entries.filter((e) => e.tags?.includes(options.tag!));\n }\n\n if (options?.limit) {\n entries = entries.slice(0, options.limit);\n }\n\n return entries;\n }\n\n async delete(slug: string): Promise<void> {\n const deleted = await this.backend.delete(slug);\n if (!deleted) {\n throw new SlugNotFoundError(slug);\n }\n }\n\n async update(slug: string, updates: Partial<ShortUrl>): Promise<ShortUrl> {\n const existing = await this.backend.get(slug);\n if (!existing) {\n throw new SlugNotFoundError(slug);\n }\n\n // If slug is changing, delete old and create new\n const newSlug = updates.slug ?? slug;\n if (newSlug !== slug) {\n await this.backend.delete(slug);\n } else {\n // Delete old to allow re-set\n await this.backend.delete(slug);\n }\n\n const updated: ShortUrl = { ...existing, ...updates, slug: newSlug };\n await this.backend.set(updated);\n return updated;\n }\n\n async getStats(_slug: string): Promise<LinkStats | null> {\n // JsonBackend has no click tracking\n return null;\n }\n}\n"],"mappings":"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","names":[]}