stow-cli 1.0.0 → 1.0.2

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,239 @@
1
+ import {
2
+ apiRequest
3
+ } from "./chunk-LYCXXF2T.js";
4
+ import {
5
+ formatBytes
6
+ } from "./chunk-ZDVARBCV.js";
7
+ import "./chunk-OZ7QQTIZ.js";
8
+
9
+ // src/interactive/app.tsx
10
+ import { render } from "ink";
11
+ import { useEffect as useEffect3, useState as useState3 } from "react";
12
+
13
+ // src/interactive/components/bucket-list.tsx
14
+ import { Box as Box3, Text as Text3, useApp, useInput } from "ink";
15
+ import Spinner from "ink-spinner";
16
+ import { useEffect, useState } from "react";
17
+
18
+ // src/interactive/components/header.tsx
19
+ import { Box, Text } from "ink";
20
+ import { jsx, jsxs } from "react/jsx-runtime";
21
+ function Header({ path, email, size }) {
22
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
23
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", width: "100%", children: [
24
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
25
+ "STOW",
26
+ path ? ` > ${path}` : ""
27
+ ] }),
28
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: size || email || "" })
29
+ ] }),
30
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) })
31
+ ] });
32
+ }
33
+
34
+ // src/interactive/components/status-bar.tsx
35
+ import { Box as Box2, Text as Text2 } from "ink";
36
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
37
+ function StatusBar({ hints }) {
38
+ return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: hints.map((h, i) => /* @__PURE__ */ jsx2(Box2, { marginRight: 2, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
39
+ "[",
40
+ h.key,
41
+ "]",
42
+ h.label,
43
+ i < hints.length - 1 ? "" : ""
44
+ ] }) }, h.key)) });
45
+ }
46
+
47
+ // src/interactive/components/bucket-list.tsx
48
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
49
+ function BucketList({ onSelect, email }) {
50
+ const { exit } = useApp();
51
+ const [buckets, setBuckets] = useState([]);
52
+ const [loading, setLoading] = useState(true);
53
+ const [error, setError] = useState(null);
54
+ const [cursor, setCursor] = useState(0);
55
+ useEffect(() => {
56
+ apiRequest("/api/buckets").then((data) => {
57
+ setBuckets(data.buckets);
58
+ setLoading(false);
59
+ }).catch((err) => {
60
+ setError(err instanceof Error ? err.message : String(err));
61
+ setLoading(false);
62
+ });
63
+ }, []);
64
+ useInput((input, key) => {
65
+ if (input === "q") {
66
+ exit();
67
+ return;
68
+ }
69
+ if (key.upArrow) {
70
+ setCursor((c) => Math.max(0, c - 1));
71
+ } else if (key.downArrow) {
72
+ setCursor((c) => Math.min(buckets.length - 1, c + 1));
73
+ } else if (key.return && buckets.length > 0) {
74
+ onSelect(buckets[cursor]);
75
+ }
76
+ });
77
+ if (loading) {
78
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { children: [
79
+ /* @__PURE__ */ jsx3(Spinner, { type: "dots" }),
80
+ " Loading buckets..."
81
+ ] }) });
82
+ }
83
+ if (error) {
84
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
85
+ "Error: ",
86
+ error
87
+ ] }) });
88
+ }
89
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
90
+ /* @__PURE__ */ jsx3(Header, { email }),
91
+ buckets.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No buckets yet. Press 'n' to create one." }) : buckets.map((b, i) => /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: i === cursor ? "cyan" : void 0, children: [
92
+ i === cursor ? "\u25B8 " : " ",
93
+ b.name.padEnd(20),
94
+ `${b.fileCount} files`.padEnd(14),
95
+ formatBytes(b.usageBytes)
96
+ ] }) }, b.id)),
97
+ /* @__PURE__ */ jsx3(
98
+ StatusBar,
99
+ {
100
+ hints: [
101
+ { key: "\u2191\u2193", label: "navigate" },
102
+ { key: "\u21B5", label: "open" },
103
+ { key: "q", label: "uit" }
104
+ ]
105
+ }
106
+ )
107
+ ] });
108
+ }
109
+
110
+ // src/interactive/components/file-list.tsx
111
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
112
+ import Spinner2 from "ink-spinner";
113
+ import { useEffect as useEffect2, useState as useState2 } from "react";
114
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
115
+ function FileList({ bucket, onBack }) {
116
+ const [files, setFiles] = useState2([]);
117
+ const [loading, setLoading] = useState2(true);
118
+ const [error, setError] = useState2(null);
119
+ const [cursor, setCursor] = useState2(0);
120
+ const [nextCursor, setNextCursor] = useState2(null);
121
+ const [copied, setCopied] = useState2(false);
122
+ useEffect2(() => {
123
+ loadFiles();
124
+ }, []);
125
+ function loadFiles(pageCursor) {
126
+ setLoading(true);
127
+ const params = new URLSearchParams({ bucket: bucket.name });
128
+ if (pageCursor) {
129
+ params.set("cursor", pageCursor);
130
+ }
131
+ apiRequest(
132
+ `/api/files?${params}`
133
+ ).then((data) => {
134
+ setFiles(
135
+ (prev) => pageCursor ? [...prev, ...data.files] : data.files
136
+ );
137
+ setNextCursor(data.nextCursor);
138
+ setLoading(false);
139
+ }).catch((err) => {
140
+ setError(err instanceof Error ? err.message : String(err));
141
+ setLoading(false);
142
+ });
143
+ }
144
+ useInput2((input, key) => {
145
+ if (key.escape || key.backspace || key.leftArrow && !loading) {
146
+ onBack();
147
+ return;
148
+ }
149
+ if (key.upArrow) {
150
+ setCursor((c) => Math.max(0, c - 1));
151
+ } else if (key.downArrow) {
152
+ setCursor((c) => {
153
+ const next = Math.min(files.length - 1, c + 1);
154
+ if (next >= files.length - 3 && nextCursor && !loading) {
155
+ loadFiles(nextCursor);
156
+ }
157
+ return next;
158
+ });
159
+ } else if (input === "c" && files.length > 0) {
160
+ import("clipboardy").then(({ default: clipboard }) => {
161
+ clipboard.write(files[cursor].url).then(() => {
162
+ setCopied(true);
163
+ setTimeout(() => setCopied(false), 2e3);
164
+ });
165
+ });
166
+ }
167
+ });
168
+ if (error) {
169
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
170
+ /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
171
+ "Error: ",
172
+ error
173
+ ] }),
174
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press Esc to go back." })
175
+ ] });
176
+ }
177
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
178
+ /* @__PURE__ */ jsx4(Header, { path: bucket.name, size: formatBytes(bucket.usageBytes) }),
179
+ loading && files.length === 0 && /* @__PURE__ */ jsxs4(Text4, { children: [
180
+ /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
181
+ " Loading files..."
182
+ ] }),
183
+ !loading && files.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No files in this bucket." }),
184
+ files.length > 0 && files.map((f, i) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color: i === cursor ? "cyan" : void 0, children: [
185
+ i === cursor ? "\u25B8 " : " ",
186
+ f.key.padEnd(28),
187
+ formatBytes(f.size).padEnd(10),
188
+ f.lastModified.split("T")[0]
189
+ ] }) }, f.key)),
190
+ loading && files.length > 0 && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
191
+ /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
192
+ " Loading more..."
193
+ ] }),
194
+ copied && /* @__PURE__ */ jsx4(Text4, { color: "green", children: "Copied URL to clipboard!" }),
195
+ /* @__PURE__ */ jsx4(
196
+ StatusBar,
197
+ {
198
+ hints: [
199
+ { key: "\u2191\u2193", label: "navigate" },
200
+ { key: "c", label: "opy url" },
201
+ { key: "\u2190", label: "back" }
202
+ ]
203
+ }
204
+ )
205
+ ] });
206
+ }
207
+
208
+ // src/interactive/app.tsx
209
+ import { jsx as jsx5 } from "react/jsx-runtime";
210
+ function App() {
211
+ const [screen, setScreen] = useState3({ type: "buckets" });
212
+ const [email, setEmail] = useState3();
213
+ useEffect3(() => {
214
+ apiRequest("/api/whoami").then((data) => setEmail(data.user.email)).catch(() => {
215
+ });
216
+ }, []);
217
+ if (screen.type === "files") {
218
+ return /* @__PURE__ */ jsx5(
219
+ FileList,
220
+ {
221
+ bucket: screen.bucket,
222
+ onBack: () => setScreen({ type: "buckets" })
223
+ }
224
+ );
225
+ }
226
+ return /* @__PURE__ */ jsx5(
227
+ BucketList,
228
+ {
229
+ email,
230
+ onSelect: (bucket) => setScreen({ type: "files", bucket })
231
+ }
232
+ );
233
+ }
234
+ function startInteractive() {
235
+ render(/* @__PURE__ */ jsx5(App, {}));
236
+ }
237
+ export {
238
+ startInteractive
239
+ };
@@ -0,0 +1,233 @@
1
+ import {
2
+ formatBytes
3
+ } from "./chunk-MHRMBH4Y.js";
4
+ import {
5
+ apiRequest
6
+ } from "./chunk-YRHPOFJT.js";
7
+ import "./chunk-2AORPTQB.js";
8
+
9
+ // src/interactive/app.tsx
10
+ import { useState as useState3, useEffect as useEffect3 } from "react";
11
+ import { render } from "ink";
12
+
13
+ // src/interactive/components/bucket-list.tsx
14
+ import { useState, useEffect } from "react";
15
+ import { Box as Box3, Text as Text3, useInput, useApp } from "ink";
16
+ import Spinner from "ink-spinner";
17
+
18
+ // src/interactive/components/header.tsx
19
+ import { Box, Text } from "ink";
20
+ import { jsx, jsxs } from "react/jsx-runtime";
21
+ function Header({ path, email, size }) {
22
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
23
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", width: "100%", children: [
24
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
25
+ "STOW",
26
+ path ? ` > ${path}` : ""
27
+ ] }),
28
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: size || email || "" })
29
+ ] }),
30
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) })
31
+ ] });
32
+ }
33
+
34
+ // src/interactive/components/status-bar.tsx
35
+ import { Box as Box2, Text as Text2 } from "ink";
36
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
37
+ function StatusBar({ hints }) {
38
+ return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: hints.map((h, i) => /* @__PURE__ */ jsx2(Box2, { marginRight: 2, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
39
+ "[",
40
+ h.key,
41
+ "]",
42
+ h.label,
43
+ i < hints.length - 1 ? "" : ""
44
+ ] }) }, h.key)) });
45
+ }
46
+
47
+ // src/interactive/components/bucket-list.tsx
48
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
49
+ function BucketList({ onSelect, email }) {
50
+ const { exit } = useApp();
51
+ const [buckets, setBuckets] = useState([]);
52
+ const [loading, setLoading] = useState(true);
53
+ const [error, setError] = useState(null);
54
+ const [cursor, setCursor] = useState(0);
55
+ useEffect(() => {
56
+ apiRequest("/api/buckets").then((data) => {
57
+ setBuckets(data.buckets);
58
+ setLoading(false);
59
+ }).catch((err) => {
60
+ setError(err instanceof Error ? err.message : String(err));
61
+ setLoading(false);
62
+ });
63
+ }, []);
64
+ useInput((input, key) => {
65
+ if (input === "q") {
66
+ exit();
67
+ return;
68
+ }
69
+ if (key.upArrow) {
70
+ setCursor((c) => Math.max(0, c - 1));
71
+ } else if (key.downArrow) {
72
+ setCursor((c) => Math.min(buckets.length - 1, c + 1));
73
+ } else if (key.return && buckets.length > 0) {
74
+ onSelect(buckets[cursor]);
75
+ }
76
+ });
77
+ if (loading) {
78
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { children: [
79
+ /* @__PURE__ */ jsx3(Spinner, { type: "dots" }),
80
+ " Loading buckets..."
81
+ ] }) });
82
+ }
83
+ if (error) {
84
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
85
+ "Error: ",
86
+ error
87
+ ] }) });
88
+ }
89
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
90
+ /* @__PURE__ */ jsx3(Header, { email }),
91
+ buckets.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No buckets yet. Press 'n' to create one." }) : buckets.map((b, i) => /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: i === cursor ? "cyan" : void 0, children: [
92
+ i === cursor ? "\u25B8 " : " ",
93
+ b.name.padEnd(20),
94
+ `${b.fileCount} files`.padEnd(14),
95
+ formatBytes(b.usageBytes)
96
+ ] }) }, b.id)),
97
+ /* @__PURE__ */ jsx3(
98
+ StatusBar,
99
+ {
100
+ hints: [
101
+ { key: "\u2191\u2193", label: "navigate" },
102
+ { key: "\u21B5", label: "open" },
103
+ { key: "q", label: "uit" }
104
+ ]
105
+ }
106
+ )
107
+ ] });
108
+ }
109
+
110
+ // src/interactive/components/file-list.tsx
111
+ import { useState as useState2, useEffect as useEffect2 } from "react";
112
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
113
+ import Spinner2 from "ink-spinner";
114
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
115
+ function FileList({ bucket, onBack }) {
116
+ const [files, setFiles] = useState2([]);
117
+ const [loading, setLoading] = useState2(true);
118
+ const [error, setError] = useState2(null);
119
+ const [cursor, setCursor] = useState2(0);
120
+ const [nextCursor, setNextCursor] = useState2(null);
121
+ const [copied, setCopied] = useState2(false);
122
+ useEffect2(() => {
123
+ loadFiles();
124
+ }, []);
125
+ function loadFiles(pageCursor) {
126
+ setLoading(true);
127
+ const params = new URLSearchParams({ bucket: bucket.name });
128
+ if (pageCursor) params.set("cursor", pageCursor);
129
+ apiRequest(
130
+ `/api/files?${params}`
131
+ ).then((data) => {
132
+ setFiles((prev) => pageCursor ? [...prev, ...data.files] : data.files);
133
+ setNextCursor(data.nextCursor);
134
+ setLoading(false);
135
+ }).catch((err) => {
136
+ setError(err instanceof Error ? err.message : String(err));
137
+ setLoading(false);
138
+ });
139
+ }
140
+ useInput2((input, key) => {
141
+ if (key.escape || key.backspace || key.leftArrow && !loading) {
142
+ onBack();
143
+ return;
144
+ }
145
+ if (key.upArrow) {
146
+ setCursor((c) => Math.max(0, c - 1));
147
+ } else if (key.downArrow) {
148
+ setCursor((c) => {
149
+ const next = Math.min(files.length - 1, c + 1);
150
+ if (next >= files.length - 3 && nextCursor && !loading) {
151
+ loadFiles(nextCursor);
152
+ }
153
+ return next;
154
+ });
155
+ } else if (input === "c" && files.length > 0) {
156
+ import("clipboardy").then(({ default: clipboard }) => {
157
+ clipboard.write(files[cursor].url).then(() => {
158
+ setCopied(true);
159
+ setTimeout(() => setCopied(false), 2e3);
160
+ });
161
+ });
162
+ }
163
+ });
164
+ if (error) {
165
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
166
+ /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
167
+ "Error: ",
168
+ error
169
+ ] }),
170
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press Esc to go back." })
171
+ ] });
172
+ }
173
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
174
+ /* @__PURE__ */ jsx4(Header, { path: bucket.name, size: formatBytes(bucket.usageBytes) }),
175
+ loading && files.length === 0 ? /* @__PURE__ */ jsxs4(Text4, { children: [
176
+ /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
177
+ " Loading files..."
178
+ ] }) : files.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No files in this bucket." }) : files.map((f, i) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color: i === cursor ? "cyan" : void 0, children: [
179
+ i === cursor ? "\u25B8 " : " ",
180
+ f.key.padEnd(28),
181
+ formatBytes(f.size).padEnd(10),
182
+ f.lastModified.split("T")[0]
183
+ ] }) }, f.key)),
184
+ loading && files.length > 0 && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
185
+ /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
186
+ " Loading more..."
187
+ ] }),
188
+ copied && /* @__PURE__ */ jsx4(Text4, { color: "green", children: "Copied URL to clipboard!" }),
189
+ /* @__PURE__ */ jsx4(
190
+ StatusBar,
191
+ {
192
+ hints: [
193
+ { key: "\u2191\u2193", label: "navigate" },
194
+ { key: "c", label: "opy url" },
195
+ { key: "\u2190", label: "back" }
196
+ ]
197
+ }
198
+ )
199
+ ] });
200
+ }
201
+
202
+ // src/interactive/app.tsx
203
+ import { jsx as jsx5 } from "react/jsx-runtime";
204
+ function App() {
205
+ const [screen, setScreen] = useState3({ type: "buckets" });
206
+ const [email, setEmail] = useState3();
207
+ useEffect3(() => {
208
+ apiRequest("/api/whoami").then((data) => setEmail(data.user.email)).catch(() => {
209
+ });
210
+ }, []);
211
+ if (screen.type === "files") {
212
+ return /* @__PURE__ */ jsx5(
213
+ FileList,
214
+ {
215
+ bucket: screen.bucket,
216
+ onBack: () => setScreen({ type: "buckets" })
217
+ }
218
+ );
219
+ }
220
+ return /* @__PURE__ */ jsx5(
221
+ BucketList,
222
+ {
223
+ email,
224
+ onSelect: (bucket) => setScreen({ type: "files", bucket })
225
+ }
226
+ );
227
+ }
228
+ function startInteractive() {
229
+ render(/* @__PURE__ */ jsx5(App, {}));
230
+ }
231
+ export {
232
+ startInteractive
233
+ };
@@ -0,0 +1,23 @@
1
+ // src/lib/config.ts
2
+ var DEFAULT_BASE_URL = "https://stow.sh";
3
+ function getApiKey() {
4
+ const key = process.env.STOW_API_KEY;
5
+ if (!key) {
6
+ console.error("Error: STOW_API_KEY environment variable not set");
7
+ console.error("");
8
+ console.error("Set your API key:");
9
+ console.error(" export STOW_API_KEY=stow_...");
10
+ console.error("");
11
+ console.error("Get an API key at https://stow.sh/dashboard/api-keys");
12
+ process.exit(1);
13
+ }
14
+ return key;
15
+ }
16
+ function getBaseUrl() {
17
+ return process.env.STOW_API_URL || DEFAULT_BASE_URL;
18
+ }
19
+
20
+ export {
21
+ getApiKey,
22
+ getBaseUrl
23
+ };
@@ -0,0 +1,125 @@
1
+ import {
2
+ getApiKey,
3
+ getBaseUrl
4
+ } from "./chunk-2AORPTQB.js";
5
+
6
+ // src/lib/api.ts
7
+ var StowApiError = class extends Error {
8
+ status;
9
+ code;
10
+ constructor(status, message, code) {
11
+ super(message);
12
+ this.name = "StowApiError";
13
+ this.status = status;
14
+ this.code = code;
15
+ }
16
+ };
17
+ function formatApiError(status, message) {
18
+ switch (status) {
19
+ case 401:
20
+ return "Invalid API key. Set STOW_API_KEY or get one at stow.sh/dashboard/api-keys";
21
+ case 403:
22
+ return message || "Permission denied";
23
+ case 404:
24
+ return message || "Not found";
25
+ case 413:
26
+ return "File too large (max 50MB)";
27
+ case 429:
28
+ return "Rate limit exceeded. Try again shortly.";
29
+ default:
30
+ return message || `Request failed (${status})`;
31
+ }
32
+ }
33
+ async function apiRequest(path, options = {}) {
34
+ const apiKey = getApiKey();
35
+ const baseUrl = getBaseUrl();
36
+ const url = `${baseUrl}${path}`;
37
+ const headers = {
38
+ "x-api-key": apiKey,
39
+ ...options.headers || {}
40
+ };
41
+ if (options.body && typeof options.body === "string") {
42
+ headers["Content-Type"] = "application/json";
43
+ }
44
+ let response;
45
+ try {
46
+ response = await fetch(url, { ...options, headers });
47
+ } catch (err) {
48
+ const msg = err instanceof Error ? err.message : String(err);
49
+ if (msg.includes("ECONNREFUSED")) {
50
+ throw new StowApiError(
51
+ 0,
52
+ `Cannot connect to ${baseUrl}. Is the server running?`
53
+ );
54
+ }
55
+ if (msg.includes("ENOTFOUND")) {
56
+ throw new StowApiError(
57
+ 0,
58
+ `Cannot resolve ${baseUrl}. Check your STOW_API_URL or internet connection.`
59
+ );
60
+ }
61
+ throw new StowApiError(0, `Network error: ${msg}`);
62
+ }
63
+ if (!response.ok) {
64
+ const data = await response.json().catch(() => ({}));
65
+ const serverMsg = data.error;
66
+ const code = data.code;
67
+ throw new StowApiError(
68
+ response.status,
69
+ formatApiError(response.status, serverMsg || ""),
70
+ code
71
+ );
72
+ }
73
+ return await response.json();
74
+ }
75
+ async function apiUpload(path, fileBuffer, filename, contentType) {
76
+ const apiKey = getApiKey();
77
+ const baseUrl = getBaseUrl();
78
+ const boundary = `----stow${Date.now()}`;
79
+ const parts = [];
80
+ parts.push(Buffer.from(`--${boundary}\r
81
+ `));
82
+ parts.push(
83
+ Buffer.from(
84
+ `Content-Disposition: form-data; name="file"; filename="${filename}"\r
85
+ `
86
+ )
87
+ );
88
+ parts.push(Buffer.from(`Content-Type: ${contentType}\r
89
+ \r
90
+ `));
91
+ parts.push(fileBuffer);
92
+ parts.push(Buffer.from(`\r
93
+ --${boundary}--\r
94
+ `));
95
+ const body = Buffer.concat(parts);
96
+ let response;
97
+ try {
98
+ response = await fetch(`${baseUrl}${path}`, {
99
+ method: "POST",
100
+ headers: {
101
+ "x-api-key": apiKey,
102
+ "Content-Type": `multipart/form-data; boundary=${boundary}`
103
+ },
104
+ body
105
+ });
106
+ } catch (err) {
107
+ const msg = err instanceof Error ? err.message : String(err);
108
+ throw new StowApiError(0, `Network error: ${msg}`);
109
+ }
110
+ if (!response.ok) {
111
+ const data = await response.json().catch(() => ({}));
112
+ const serverMsg = data.error;
113
+ throw new StowApiError(
114
+ response.status,
115
+ formatApiError(response.status, serverMsg || "")
116
+ );
117
+ }
118
+ return await response.json();
119
+ }
120
+
121
+ export {
122
+ StowApiError,
123
+ apiRequest,
124
+ apiUpload
125
+ };
@@ -0,0 +1,79 @@
1
+ import {
2
+ getApiKey,
3
+ getBaseUrl
4
+ } from "./chunk-OZ7QQTIZ.js";
5
+
6
+ // src/lib/api.ts
7
+ var StowApiError = class extends Error {
8
+ status;
9
+ code;
10
+ constructor(status, message, code) {
11
+ super(message);
12
+ this.name = "StowApiError";
13
+ this.status = status;
14
+ this.code = code;
15
+ }
16
+ };
17
+ function formatApiError(status, message) {
18
+ switch (status) {
19
+ case 401:
20
+ return "Invalid API key. Set STOW_API_KEY or get one at stow.sh/dashboard/api-keys";
21
+ case 403:
22
+ return message || "Permission denied";
23
+ case 404:
24
+ return message || "Not found";
25
+ case 413:
26
+ return "File too large (max 50MB)";
27
+ case 429:
28
+ return "Rate limit exceeded. Try again shortly.";
29
+ default:
30
+ return message || `Request failed (${status})`;
31
+ }
32
+ }
33
+ async function apiRequest(path, options = {}) {
34
+ const apiKey = getApiKey();
35
+ const baseUrl = getBaseUrl();
36
+ const url = `${baseUrl}${path}`;
37
+ const headers = {
38
+ "x-api-key": apiKey,
39
+ ...options.headers || {}
40
+ };
41
+ if (options.body && typeof options.body === "string") {
42
+ headers["Content-Type"] = "application/json";
43
+ }
44
+ let response;
45
+ try {
46
+ response = await fetch(url, { ...options, headers });
47
+ } catch (err) {
48
+ const msg = err instanceof Error ? err.message : String(err);
49
+ if (msg.includes("ECONNREFUSED")) {
50
+ throw new StowApiError(
51
+ 0,
52
+ `Cannot connect to ${baseUrl}. Is the server running?`
53
+ );
54
+ }
55
+ if (msg.includes("ENOTFOUND")) {
56
+ throw new StowApiError(
57
+ 0,
58
+ `Cannot resolve ${baseUrl}. Check your STOW_API_URL or internet connection.`
59
+ );
60
+ }
61
+ throw new StowApiError(0, `Network error: ${msg}`);
62
+ }
63
+ if (!response.ok) {
64
+ const data = await response.json().catch(() => ({}));
65
+ const serverMsg = data.error;
66
+ const code = data.code;
67
+ throw new StowApiError(
68
+ response.status,
69
+ formatApiError(response.status, serverMsg || ""),
70
+ code
71
+ );
72
+ }
73
+ return await response.json();
74
+ }
75
+
76
+ export {
77
+ StowApiError,
78
+ apiRequest
79
+ };