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
+ formatBytes
3
+ } from "./chunk-ZDVARBCV.js";
4
+ import {
5
+ apiRequest
6
+ } from "./chunk-FEMMZ4YZ.js";
7
+ import "./chunk-2AORPTQB.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,239 @@
1
+ import {
2
+ formatBytes
3
+ } from "./chunk-ZDVARBCV.js";
4
+ import {
5
+ apiRequest
6
+ } from "./chunk-R5CCBTXZ.js";
7
+ import "./chunk-2AORPTQB.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
+ };