stow-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-2A2CFVBC.js +249 -0
- package/dist/chunk-JYOMHKFS.js +17 -0
- package/dist/chunk-OZ7QQTIZ.js +23 -0
- package/dist/chunk-ZDVARBCV.js +43 -0
- package/dist/cli.js +238 -0
- package/dist/delete-NAV6P5O5.js +14 -0
- package/dist/list-KEQPJY7I.js +109 -0
- package/dist/open-F3LHXI3R.js +15 -0
- package/dist/upload-DQDBDIDI.js +92 -0
- package/dist/whoami-754O5IML.js +28 -0
- package/package.json +50 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatBytes
|
|
3
|
+
} from "./chunk-ZDVARBCV.js";
|
|
4
|
+
import {
|
|
5
|
+
createStow
|
|
6
|
+
} from "./chunk-JYOMHKFS.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 toBucket(result) {
|
|
50
|
+
return {
|
|
51
|
+
id: result.id,
|
|
52
|
+
name: result.name,
|
|
53
|
+
description: result.description ?? null,
|
|
54
|
+
fileCount: result.fileCount ?? 0,
|
|
55
|
+
isPublic: result.isPublic ?? false,
|
|
56
|
+
usageBytes: result.usageBytes ?? 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function BucketList({ onSelect, email }) {
|
|
60
|
+
const { exit } = useApp();
|
|
61
|
+
const [buckets, setBuckets] = useState([]);
|
|
62
|
+
const [loading, setLoading] = useState(true);
|
|
63
|
+
const [error, setError] = useState(null);
|
|
64
|
+
const [cursor, setCursor] = useState(0);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const stow = createStow();
|
|
67
|
+
stow.listBuckets().then((data) => {
|
|
68
|
+
setBuckets(data.buckets.map(toBucket));
|
|
69
|
+
setLoading(false);
|
|
70
|
+
}).catch((err) => {
|
|
71
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
72
|
+
setLoading(false);
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
75
|
+
useInput((input, key) => {
|
|
76
|
+
if (input === "q") {
|
|
77
|
+
exit();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (key.upArrow) {
|
|
81
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
82
|
+
} else if (key.downArrow) {
|
|
83
|
+
setCursor((c) => Math.min(buckets.length - 1, c + 1));
|
|
84
|
+
} else if (key.return && buckets.length > 0) {
|
|
85
|
+
onSelect(buckets[cursor]);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
if (loading) {
|
|
89
|
+
return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
90
|
+
/* @__PURE__ */ jsx3(Spinner, { type: "dots" }),
|
|
91
|
+
" Loading buckets..."
|
|
92
|
+
] }) });
|
|
93
|
+
}
|
|
94
|
+
if (error) {
|
|
95
|
+
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
|
|
96
|
+
"Error: ",
|
|
97
|
+
error
|
|
98
|
+
] }) });
|
|
99
|
+
}
|
|
100
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
101
|
+
/* @__PURE__ */ jsx3(Header, { email }),
|
|
102
|
+
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: [
|
|
103
|
+
i === cursor ? "\u25B8 " : " ",
|
|
104
|
+
b.name.padEnd(20),
|
|
105
|
+
`${b.fileCount} files`.padEnd(14),
|
|
106
|
+
formatBytes(b.usageBytes)
|
|
107
|
+
] }) }, b.id)),
|
|
108
|
+
/* @__PURE__ */ jsx3(
|
|
109
|
+
StatusBar,
|
|
110
|
+
{
|
|
111
|
+
hints: [
|
|
112
|
+
{ key: "\u2191\u2193", label: "navigate" },
|
|
113
|
+
{ key: "\u21B5", label: "open" },
|
|
114
|
+
{ key: "q", label: "uit" }
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
] });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/interactive/components/file-list.tsx
|
|
122
|
+
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
123
|
+
import Spinner2 from "ink-spinner";
|
|
124
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
125
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
126
|
+
function FileList({ bucket, onBack }) {
|
|
127
|
+
const [files, setFiles] = useState2([]);
|
|
128
|
+
const [loading, setLoading] = useState2(true);
|
|
129
|
+
const [error, setError] = useState2(null);
|
|
130
|
+
const [cursor, setCursor] = useState2(0);
|
|
131
|
+
const [nextCursor, setNextCursor] = useState2(null);
|
|
132
|
+
const [copied, setCopied] = useState2(false);
|
|
133
|
+
useEffect2(() => {
|
|
134
|
+
loadFiles();
|
|
135
|
+
}, []);
|
|
136
|
+
function loadFiles(pageCursor) {
|
|
137
|
+
setLoading(true);
|
|
138
|
+
const stow = createStow();
|
|
139
|
+
stow.listFiles({
|
|
140
|
+
bucket: bucket.name,
|
|
141
|
+
...pageCursor ? { cursor: pageCursor } : {}
|
|
142
|
+
}).then((data) => {
|
|
143
|
+
setFiles(
|
|
144
|
+
(prev) => pageCursor ? [...prev, ...data.files] : data.files
|
|
145
|
+
);
|
|
146
|
+
setNextCursor(data.nextCursor);
|
|
147
|
+
setLoading(false);
|
|
148
|
+
}).catch((err) => {
|
|
149
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
150
|
+
setLoading(false);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
useInput2((input, key) => {
|
|
154
|
+
if (key.escape || key.backspace || key.leftArrow && !loading) {
|
|
155
|
+
onBack();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (key.upArrow) {
|
|
159
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
160
|
+
} else if (key.downArrow) {
|
|
161
|
+
setCursor((c) => {
|
|
162
|
+
const next = Math.min(files.length - 1, c + 1);
|
|
163
|
+
if (next >= files.length - 3 && nextCursor && !loading) {
|
|
164
|
+
loadFiles(nextCursor);
|
|
165
|
+
}
|
|
166
|
+
return next;
|
|
167
|
+
});
|
|
168
|
+
} else if (input === "c" && files.length > 0) {
|
|
169
|
+
import("clipboardy").then(({ default: clipboard }) => {
|
|
170
|
+
clipboard.write(files[cursor].url).then(() => {
|
|
171
|
+
setCopied(true);
|
|
172
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
if (error) {
|
|
178
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
179
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
|
|
180
|
+
"Error: ",
|
|
181
|
+
error
|
|
182
|
+
] }),
|
|
183
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press Esc to go back." })
|
|
184
|
+
] });
|
|
185
|
+
}
|
|
186
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
187
|
+
/* @__PURE__ */ jsx4(Header, { path: bucket.name, size: formatBytes(bucket.usageBytes) }),
|
|
188
|
+
loading && files.length === 0 && /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
189
|
+
/* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
|
|
190
|
+
" Loading files..."
|
|
191
|
+
] }),
|
|
192
|
+
!loading && files.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No files in this bucket." }),
|
|
193
|
+
files.length > 0 && files.map((f, i) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color: i === cursor ? "cyan" : void 0, children: [
|
|
194
|
+
i === cursor ? "\u25B8 " : " ",
|
|
195
|
+
f.key.padEnd(28),
|
|
196
|
+
formatBytes(f.size).padEnd(10),
|
|
197
|
+
f.lastModified.split("T")[0]
|
|
198
|
+
] }) }, f.key)),
|
|
199
|
+
loading && files.length > 0 && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
200
|
+
/* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
|
|
201
|
+
" Loading more..."
|
|
202
|
+
] }),
|
|
203
|
+
copied && /* @__PURE__ */ jsx4(Text4, { color: "green", children: "Copied URL to clipboard!" }),
|
|
204
|
+
/* @__PURE__ */ jsx4(
|
|
205
|
+
StatusBar,
|
|
206
|
+
{
|
|
207
|
+
hints: [
|
|
208
|
+
{ key: "\u2191\u2193", label: "navigate" },
|
|
209
|
+
{ key: "c", label: "opy url" },
|
|
210
|
+
{ key: "\u2190", label: "back" }
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
] });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/interactive/app.tsx
|
|
218
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
219
|
+
function App() {
|
|
220
|
+
const [screen, setScreen] = useState3({ type: "buckets" });
|
|
221
|
+
const [email, setEmail] = useState3();
|
|
222
|
+
useEffect3(() => {
|
|
223
|
+
const stow = createStow();
|
|
224
|
+
stow.whoami().then((data) => setEmail(data.user.email)).catch(() => {
|
|
225
|
+
});
|
|
226
|
+
}, []);
|
|
227
|
+
if (screen.type === "files") {
|
|
228
|
+
return /* @__PURE__ */ jsx5(
|
|
229
|
+
FileList,
|
|
230
|
+
{
|
|
231
|
+
bucket: screen.bucket,
|
|
232
|
+
onBack: () => setScreen({ type: "buckets" })
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return /* @__PURE__ */ jsx5(
|
|
237
|
+
BucketList,
|
|
238
|
+
{
|
|
239
|
+
email,
|
|
240
|
+
onSelect: (bucket) => setScreen({ type: "files", bucket })
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
function startInteractive() {
|
|
245
|
+
render(/* @__PURE__ */ jsx5(App, {}));
|
|
246
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
startInteractive
|
|
249
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getApiKey,
|
|
3
|
+
getBaseUrl
|
|
4
|
+
} from "./chunk-OZ7QQTIZ.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/stow.ts
|
|
7
|
+
import { StowServer } from "@howells/stow-server";
|
|
8
|
+
function createStow() {
|
|
9
|
+
return new StowServer({
|
|
10
|
+
apiKey: getApiKey(),
|
|
11
|
+
baseUrl: getBaseUrl()
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
createStow
|
|
17
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/lib/config.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://app.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,43 @@
|
|
|
1
|
+
// src/lib/format.ts
|
|
2
|
+
function formatBytes(bytes) {
|
|
3
|
+
if (bytes === 0) {
|
|
4
|
+
return "0 B";
|
|
5
|
+
}
|
|
6
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
7
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
8
|
+
return `${(bytes / 1024 ** i).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
|
|
9
|
+
}
|
|
10
|
+
function formatTable(headers, rows) {
|
|
11
|
+
if (rows.length === 0) {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
const widths = headers.map((h, i) => {
|
|
15
|
+
const dataMax = rows.reduce(
|
|
16
|
+
(max, row) => Math.max(max, (row[i] || "").length),
|
|
17
|
+
0
|
|
18
|
+
);
|
|
19
|
+
return Math.max(h.length, dataMax);
|
|
20
|
+
});
|
|
21
|
+
const pad = (str, width) => str.padEnd(width);
|
|
22
|
+
const sep = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u2500");
|
|
23
|
+
const lines = [];
|
|
24
|
+
lines.push(headers.map((h, i) => pad(h, widths[i])).join(" "));
|
|
25
|
+
lines.push(sep);
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
lines.push(row.map((cell, i) => pad(cell || "", widths[i])).join(" "));
|
|
28
|
+
}
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
31
|
+
function usageBar(used, total, width = 20) {
|
|
32
|
+
const ratio = Math.min(used / total, 1);
|
|
33
|
+
const filled = Math.round(ratio * width);
|
|
34
|
+
const empty = width - filled;
|
|
35
|
+
const pct = Math.round(ratio * 100);
|
|
36
|
+
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}] ${pct}%`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
formatBytes,
|
|
41
|
+
formatTable,
|
|
42
|
+
usageBar
|
|
43
|
+
};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { StowError } from "@howells/stow-server";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/lib/cli-docs.ts
|
|
8
|
+
var CLI_DOCS = {
|
|
9
|
+
root: {
|
|
10
|
+
description: "CLI for Stow file storage",
|
|
11
|
+
usage: "stow [command] [options]",
|
|
12
|
+
examples: [
|
|
13
|
+
"stow whoami",
|
|
14
|
+
"stow upload ./photo.jpg --bucket photos",
|
|
15
|
+
"stow drop ./screenshot.png",
|
|
16
|
+
"stow buckets",
|
|
17
|
+
"stow files photos --limit 50"
|
|
18
|
+
],
|
|
19
|
+
notes: [
|
|
20
|
+
"Set STOW_API_KEY before running commands that call the API.",
|
|
21
|
+
"Set STOW_API_URL to target a non-default environment."
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
drop: {
|
|
25
|
+
description: "Upload a file and get a short URL (quick share)",
|
|
26
|
+
usage: "stow drop <file> [options]",
|
|
27
|
+
examples: ["stow drop ./video.mp4", "stow drop ./notes.txt --quiet"]
|
|
28
|
+
},
|
|
29
|
+
upload: {
|
|
30
|
+
description: "Upload a file to a bucket",
|
|
31
|
+
usage: "stow upload <file> [options]",
|
|
32
|
+
examples: [
|
|
33
|
+
"stow upload ./logo.png --bucket brand-assets",
|
|
34
|
+
"stow upload ./clip.mov --quiet"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
buckets: {
|
|
38
|
+
description: "List your buckets",
|
|
39
|
+
usage: "stow buckets",
|
|
40
|
+
examples: ["stow buckets"]
|
|
41
|
+
},
|
|
42
|
+
bucketsCreate: {
|
|
43
|
+
description: "Create a new bucket",
|
|
44
|
+
usage: "stow buckets create <name> [options]",
|
|
45
|
+
examples: [
|
|
46
|
+
"stow buckets create photos",
|
|
47
|
+
'stow buckets create docs --description "Product docs"',
|
|
48
|
+
"stow buckets create public-media --public"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
bucketsRename: {
|
|
52
|
+
description: "Rename a bucket",
|
|
53
|
+
usage: "stow buckets rename <name> <new-name> [options]",
|
|
54
|
+
examples: ["stow buckets rename old-name new-name --yes"],
|
|
55
|
+
notes: ["Renaming a bucket can break existing public URLs."]
|
|
56
|
+
},
|
|
57
|
+
bucketsDelete: {
|
|
58
|
+
description: "Delete a bucket by ID",
|
|
59
|
+
usage: "stow buckets delete <id>",
|
|
60
|
+
examples: ["stow buckets delete 8f3d1ab4-..."]
|
|
61
|
+
},
|
|
62
|
+
files: {
|
|
63
|
+
description: "List files in a bucket",
|
|
64
|
+
usage: "stow files <bucket> [options]",
|
|
65
|
+
examples: [
|
|
66
|
+
"stow files photos",
|
|
67
|
+
"stow files photos --search avatars/ --limit 100"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
drops: {
|
|
71
|
+
description: "List your drops with usage info",
|
|
72
|
+
usage: "stow drops",
|
|
73
|
+
examples: ["stow drops"]
|
|
74
|
+
},
|
|
75
|
+
dropsDelete: {
|
|
76
|
+
description: "Delete a drop by ID",
|
|
77
|
+
usage: "stow drops delete <id>",
|
|
78
|
+
examples: ["stow drops delete drop_abc123"]
|
|
79
|
+
},
|
|
80
|
+
delete: {
|
|
81
|
+
description: "Delete a file from a bucket",
|
|
82
|
+
usage: "stow delete <bucket> <key>",
|
|
83
|
+
examples: ["stow delete photos hero/banner.png"]
|
|
84
|
+
},
|
|
85
|
+
whoami: {
|
|
86
|
+
description: "Show account info, usage stats, and API key details",
|
|
87
|
+
usage: "stow whoami",
|
|
88
|
+
examples: ["stow whoami"]
|
|
89
|
+
},
|
|
90
|
+
open: {
|
|
91
|
+
description: "Open a bucket in the browser",
|
|
92
|
+
usage: "stow open <bucket>",
|
|
93
|
+
examples: ["stow open photos"]
|
|
94
|
+
},
|
|
95
|
+
interactive: {
|
|
96
|
+
description: "Launch interactive TUI mode",
|
|
97
|
+
usage: "stow --interactive",
|
|
98
|
+
examples: ["stow", "stow --interactive"]
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
function renderCommandHelp(key) {
|
|
102
|
+
const doc = CLI_DOCS[key];
|
|
103
|
+
const lines = [
|
|
104
|
+
"",
|
|
105
|
+
`Usage: ${doc.usage}`,
|
|
106
|
+
"",
|
|
107
|
+
"Examples:",
|
|
108
|
+
...doc.examples.map((example) => ` ${example}`)
|
|
109
|
+
];
|
|
110
|
+
if (doc.notes?.length) {
|
|
111
|
+
lines.push("", "Notes:", ...doc.notes.map((note) => ` ${note}`));
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/cli.ts
|
|
117
|
+
var VERSION = "1.0.0";
|
|
118
|
+
function handleError(err) {
|
|
119
|
+
if (err instanceof StowError) {
|
|
120
|
+
console.error(`Error: ${err.message}`);
|
|
121
|
+
} else if (err instanceof Error) {
|
|
122
|
+
console.error(`Error: ${err.message}`);
|
|
123
|
+
} else {
|
|
124
|
+
console.error(`Error: ${err}`);
|
|
125
|
+
}
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
var program = new Command();
|
|
129
|
+
program.name("stow").description(CLI_DOCS.root.description).version(VERSION).addHelpText("after", renderCommandHelp("root"));
|
|
130
|
+
program.command("drop").description(CLI_DOCS.drop.description).argument("<file>", "File to upload").option("-q, --quiet", "Only output the URL").addHelpText("after", renderCommandHelp("drop")).action(async (file, options) => {
|
|
131
|
+
try {
|
|
132
|
+
const { uploadDrop } = await import("./upload-DQDBDIDI.js");
|
|
133
|
+
await uploadDrop(file, options);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
handleError(err);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
program.command("upload").description(CLI_DOCS.upload.description).argument("<file>", "File to upload").option("-b, --bucket <name>", "Bucket name or ID").option("-q, --quiet", "Only output the URL").addHelpText("after", renderCommandHelp("upload")).action(
|
|
139
|
+
async (file, options) => {
|
|
140
|
+
try {
|
|
141
|
+
const { uploadFile } = await import("./upload-DQDBDIDI.js");
|
|
142
|
+
await uploadFile(file, options);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
handleError(err);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
var bucketsCmd = program.command("buckets").description(CLI_DOCS.buckets.description).addHelpText("after", renderCommandHelp("buckets")).action(async () => {
|
|
149
|
+
try {
|
|
150
|
+
const { listBuckets } = await import("./list-KEQPJY7I.js");
|
|
151
|
+
await listBuckets();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
handleError(err);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
bucketsCmd.command("create").description(CLI_DOCS.bucketsCreate.description).argument("<name>", "Bucket name").option("-d, --description <text>", "Bucket description").option("--public", "Make bucket public").addHelpText("after", renderCommandHelp("bucketsCreate")).action(
|
|
157
|
+
async (name, options) => {
|
|
158
|
+
try {
|
|
159
|
+
const { createBucket } = await import("./list-KEQPJY7I.js");
|
|
160
|
+
await createBucket(name, options);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
handleError(err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
bucketsCmd.command("rename").description(CLI_DOCS.bucketsRename.description).argument("<name>", "Current bucket name").argument("<new-name>", "New bucket name").option("-y, --yes", "Skip confirmation warning").addHelpText("after", renderCommandHelp("bucketsRename")).action(async (name, newName, options) => {
|
|
167
|
+
try {
|
|
168
|
+
const { renameBucket } = await import("./list-KEQPJY7I.js");
|
|
169
|
+
await renameBucket(name, newName, options);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
handleError(err);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
bucketsCmd.command("delete").description(CLI_DOCS.bucketsDelete.description).argument("<id>", "Bucket ID").addHelpText("after", renderCommandHelp("bucketsDelete")).action(async (id) => {
|
|
175
|
+
try {
|
|
176
|
+
const { deleteBucket } = await import("./list-KEQPJY7I.js");
|
|
177
|
+
await deleteBucket(id);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
handleError(err);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
program.command("files").description(CLI_DOCS.files.description).argument("<bucket>", "Bucket name").option("-s, --search <prefix>", "Filter by prefix").option("-l, --limit <count>", "Max files to return").addHelpText("after", renderCommandHelp("files")).action(
|
|
183
|
+
async (bucket, options) => {
|
|
184
|
+
try {
|
|
185
|
+
const { listFiles } = await import("./list-KEQPJY7I.js");
|
|
186
|
+
await listFiles(bucket, options);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
handleError(err);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
var dropsCmd = program.command("drops").description(CLI_DOCS.drops.description).addHelpText("after", renderCommandHelp("drops")).action(async () => {
|
|
193
|
+
try {
|
|
194
|
+
const { listDrops } = await import("./list-KEQPJY7I.js");
|
|
195
|
+
await listDrops();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
handleError(err);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
dropsCmd.command("delete").description(CLI_DOCS.dropsDelete.description).argument("<id>", "Drop ID").addHelpText("after", renderCommandHelp("dropsDelete")).action(async (id) => {
|
|
201
|
+
try {
|
|
202
|
+
const { deleteDrop } = await import("./list-KEQPJY7I.js");
|
|
203
|
+
await deleteDrop(id);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
handleError(err);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
program.command("delete").description(CLI_DOCS.delete.description).argument("<bucket>", "Bucket name").argument("<key>", "File key").addHelpText("after", renderCommandHelp("delete")).action(async (bucket, key) => {
|
|
209
|
+
try {
|
|
210
|
+
const { deleteFile } = await import("./delete-NAV6P5O5.js");
|
|
211
|
+
await deleteFile(bucket, key);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
handleError(err);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
program.command("whoami").description(CLI_DOCS.whoami.description).addHelpText("after", renderCommandHelp("whoami")).action(async () => {
|
|
217
|
+
try {
|
|
218
|
+
const { whoami } = await import("./whoami-754O5IML.js");
|
|
219
|
+
await whoami();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
handleError(err);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
program.command("open").description(CLI_DOCS.open.description).argument("<bucket>", "Bucket name").addHelpText("after", renderCommandHelp("open")).action(async (bucket) => {
|
|
225
|
+
try {
|
|
226
|
+
const { openBucket } = await import("./open-F3LHXI3R.js");
|
|
227
|
+
await openBucket(bucket);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
handleError(err);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
program.option("-i, --interactive", CLI_DOCS.interactive.description).addHelpText("after", renderCommandHelp("interactive"));
|
|
233
|
+
program.parse();
|
|
234
|
+
var opts = program.opts();
|
|
235
|
+
var args = process.argv.slice(2);
|
|
236
|
+
if (args.length === 0 || args.length === 1 && opts.interactive) {
|
|
237
|
+
import("./app-2A2CFVBC.js").then(({ startInteractive }) => startInteractive()).catch(handleError);
|
|
238
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createStow
|
|
3
|
+
} from "./chunk-JYOMHKFS.js";
|
|
4
|
+
import "./chunk-OZ7QQTIZ.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/delete.ts
|
|
7
|
+
async function deleteFile(bucket, key) {
|
|
8
|
+
const stow = createStow();
|
|
9
|
+
await stow.deleteFile(key, { bucket });
|
|
10
|
+
console.log(`Deleted: ${key} from ${bucket}`);
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
deleteFile
|
|
14
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatBytes,
|
|
3
|
+
formatTable,
|
|
4
|
+
usageBar
|
|
5
|
+
} from "./chunk-ZDVARBCV.js";
|
|
6
|
+
import {
|
|
7
|
+
createStow
|
|
8
|
+
} from "./chunk-JYOMHKFS.js";
|
|
9
|
+
import "./chunk-OZ7QQTIZ.js";
|
|
10
|
+
|
|
11
|
+
// src/commands/list.ts
|
|
12
|
+
async function listBuckets() {
|
|
13
|
+
const stow = createStow();
|
|
14
|
+
const data = await stow.listBuckets();
|
|
15
|
+
if (data.buckets.length === 0) {
|
|
16
|
+
console.log("No buckets yet. Create one with: stow buckets create <name>");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const rows = data.buckets.map((b) => [
|
|
20
|
+
b.name,
|
|
21
|
+
b.isPublic ? "public" : "private",
|
|
22
|
+
`${b.fileCount ?? 0} files`,
|
|
23
|
+
formatBytes(b.usageBytes ?? 0),
|
|
24
|
+
b.description || ""
|
|
25
|
+
]);
|
|
26
|
+
console.log(
|
|
27
|
+
formatTable(["Name", "Access", "Files", "Size", "Description"], rows)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
async function createBucket(name, options) {
|
|
31
|
+
const stow = createStow();
|
|
32
|
+
const bucket = await stow.createBucket({
|
|
33
|
+
name,
|
|
34
|
+
...options.description ? { description: options.description } : {},
|
|
35
|
+
...options.public ? { isPublic: true } : {}
|
|
36
|
+
});
|
|
37
|
+
console.log(`Created bucket: ${bucket.name}`);
|
|
38
|
+
}
|
|
39
|
+
async function renameBucket(name, newName, options) {
|
|
40
|
+
if (!options.yes) {
|
|
41
|
+
console.error(
|
|
42
|
+
"Warning: Renaming a bucket will break any existing URLs using the old name."
|
|
43
|
+
);
|
|
44
|
+
console.error("Use --yes to skip this warning.");
|
|
45
|
+
}
|
|
46
|
+
const stow = createStow();
|
|
47
|
+
const bucket = await stow.renameBucket(name, newName);
|
|
48
|
+
console.log(`Renamed bucket: ${name} \u2192 ${bucket.name}`);
|
|
49
|
+
}
|
|
50
|
+
async function listFiles(bucket, options) {
|
|
51
|
+
const stow = createStow();
|
|
52
|
+
const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null;
|
|
53
|
+
const data = await stow.listFiles({
|
|
54
|
+
bucket,
|
|
55
|
+
...options.search ? { prefix: options.search } : {},
|
|
56
|
+
...parsedLimit && Number.isFinite(parsedLimit) && parsedLimit > 0 ? { limit: parsedLimit } : {}
|
|
57
|
+
});
|
|
58
|
+
if (data.files.length === 0) {
|
|
59
|
+
console.log(`No files in bucket '${bucket}'.`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const rows = data.files.map((f) => [
|
|
63
|
+
f.key,
|
|
64
|
+
formatBytes(f.size),
|
|
65
|
+
f.lastModified.split("T")[0]
|
|
66
|
+
]);
|
|
67
|
+
console.log(formatTable(["Key", "Size", "Modified"], rows));
|
|
68
|
+
if (data.nextCursor) {
|
|
69
|
+
console.log("\n(more files available \u2014 use --limit to see more)");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function listDrops() {
|
|
73
|
+
const stow = createStow();
|
|
74
|
+
const data = await stow.listDrops();
|
|
75
|
+
if (data.drops.length === 0) {
|
|
76
|
+
console.log("No drops yet. Create one with: stow drop <file>");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const rows = data.drops.map((d) => [
|
|
80
|
+
d.filename,
|
|
81
|
+
formatBytes(Number(d.size)),
|
|
82
|
+
d.contentType,
|
|
83
|
+
d.url
|
|
84
|
+
]);
|
|
85
|
+
console.log(formatTable(["Filename", "Size", "Type", "URL"], rows));
|
|
86
|
+
console.log(
|
|
87
|
+
`
|
|
88
|
+
Storage: ${usageBar(data.usage.bytes, data.usage.limit)} ${formatBytes(data.usage.bytes)} / ${formatBytes(data.usage.limit)}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
async function deleteBucket(id) {
|
|
92
|
+
const stow = createStow();
|
|
93
|
+
await stow.deleteBucket(id);
|
|
94
|
+
console.log(`Deleted bucket: ${id}`);
|
|
95
|
+
}
|
|
96
|
+
async function deleteDrop(id) {
|
|
97
|
+
const stow = createStow();
|
|
98
|
+
await stow.deleteDrop(id);
|
|
99
|
+
console.log(`Deleted drop: ${id}`);
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
createBucket,
|
|
103
|
+
deleteBucket,
|
|
104
|
+
deleteDrop,
|
|
105
|
+
listBuckets,
|
|
106
|
+
listDrops,
|
|
107
|
+
listFiles,
|
|
108
|
+
renameBucket
|
|
109
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBaseUrl
|
|
3
|
+
} from "./chunk-OZ7QQTIZ.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/open.ts
|
|
6
|
+
async function openBucket(bucket) {
|
|
7
|
+
const baseUrl = getBaseUrl();
|
|
8
|
+
const url = `${baseUrl}/dashboard/buckets/${encodeURIComponent(bucket)}`;
|
|
9
|
+
const { default: open } = await import("open");
|
|
10
|
+
await open(url);
|
|
11
|
+
console.log(`Opened: ${url}`);
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
openBucket
|
|
15
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatBytes
|
|
3
|
+
} from "./chunk-ZDVARBCV.js";
|
|
4
|
+
import {
|
|
5
|
+
createStow
|
|
6
|
+
} from "./chunk-JYOMHKFS.js";
|
|
7
|
+
import "./chunk-OZ7QQTIZ.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/upload.ts
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { basename, resolve } from "path";
|
|
12
|
+
var CONTENT_TYPES = {
|
|
13
|
+
png: "image/png",
|
|
14
|
+
jpg: "image/jpeg",
|
|
15
|
+
jpeg: "image/jpeg",
|
|
16
|
+
gif: "image/gif",
|
|
17
|
+
webp: "image/webp",
|
|
18
|
+
svg: "image/svg+xml",
|
|
19
|
+
ico: "image/x-icon",
|
|
20
|
+
avif: "image/avif",
|
|
21
|
+
pdf: "application/pdf",
|
|
22
|
+
mp4: "video/mp4",
|
|
23
|
+
webm: "video/webm",
|
|
24
|
+
mov: "video/quicktime",
|
|
25
|
+
mp3: "audio/mpeg",
|
|
26
|
+
wav: "audio/wav",
|
|
27
|
+
ogg: "audio/ogg",
|
|
28
|
+
zip: "application/zip",
|
|
29
|
+
tar: "application/x-tar",
|
|
30
|
+
gz: "application/gzip",
|
|
31
|
+
txt: "text/plain",
|
|
32
|
+
json: "application/json",
|
|
33
|
+
xml: "application/xml",
|
|
34
|
+
html: "text/html",
|
|
35
|
+
css: "text/css",
|
|
36
|
+
js: "application/javascript"
|
|
37
|
+
};
|
|
38
|
+
function getContentType(filename) {
|
|
39
|
+
const ext = filename.toLowerCase().split(".").pop();
|
|
40
|
+
return CONTENT_TYPES[ext || ""] || "application/octet-stream";
|
|
41
|
+
}
|
|
42
|
+
function readFile(filePath) {
|
|
43
|
+
const resolvedPath = resolve(filePath);
|
|
44
|
+
if (!existsSync(resolvedPath)) {
|
|
45
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const buffer = readFileSync(resolvedPath);
|
|
49
|
+
const filename = basename(resolvedPath);
|
|
50
|
+
const contentType = getContentType(filename);
|
|
51
|
+
return { buffer, filename, contentType };
|
|
52
|
+
}
|
|
53
|
+
function printUploadResult(url, options, opts) {
|
|
54
|
+
if (options.quiet) {
|
|
55
|
+
console.log(url);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
console.error(opts?.deduped ? "Done! (deduped)" : "Done!");
|
|
59
|
+
console.log("");
|
|
60
|
+
console.log(url);
|
|
61
|
+
}
|
|
62
|
+
async function uploadDrop(filePath, options) {
|
|
63
|
+
const { buffer, filename, contentType } = readFile(filePath);
|
|
64
|
+
if (!options.quiet) {
|
|
65
|
+
console.error(`Uploading ${filename} (${formatBytes(buffer.length)})...`);
|
|
66
|
+
}
|
|
67
|
+
const stow = createStow();
|
|
68
|
+
const result = await stow.drop(buffer, {
|
|
69
|
+
filename,
|
|
70
|
+
contentType
|
|
71
|
+
});
|
|
72
|
+
printUploadResult(result.url, options);
|
|
73
|
+
}
|
|
74
|
+
async function uploadFile(filePath, options) {
|
|
75
|
+
const { buffer, filename, contentType } = readFile(filePath);
|
|
76
|
+
if (!options.quiet) {
|
|
77
|
+
console.error(`Uploading ${filename} (${formatBytes(buffer.length)})...`);
|
|
78
|
+
}
|
|
79
|
+
const stow = createStow();
|
|
80
|
+
const result = await stow.uploadFile(buffer, {
|
|
81
|
+
...options.bucket ? { bucket: options.bucket } : {},
|
|
82
|
+
filename,
|
|
83
|
+
contentType
|
|
84
|
+
});
|
|
85
|
+
printUploadResult(result.url ?? result.key, options, {
|
|
86
|
+
deduped: result.deduped
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
uploadDrop,
|
|
91
|
+
uploadFile
|
|
92
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatBytes
|
|
3
|
+
} from "./chunk-ZDVARBCV.js";
|
|
4
|
+
import {
|
|
5
|
+
createStow
|
|
6
|
+
} from "./chunk-JYOMHKFS.js";
|
|
7
|
+
import "./chunk-OZ7QQTIZ.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/whoami.ts
|
|
10
|
+
async function whoami() {
|
|
11
|
+
const stow = createStow();
|
|
12
|
+
const data = await stow.whoami();
|
|
13
|
+
console.log(`Account: ${data.user.email}`);
|
|
14
|
+
console.log("");
|
|
15
|
+
console.log(`Buckets: ${data.stats.bucketCount}`);
|
|
16
|
+
console.log(`Files: ${data.stats.totalFiles}`);
|
|
17
|
+
console.log(`Storage: ${formatBytes(data.stats.totalBytes)}`);
|
|
18
|
+
if (data.key) {
|
|
19
|
+
console.log("");
|
|
20
|
+
console.log(`API Key: ${data.key.name}`);
|
|
21
|
+
console.log(`Scope: ${data.key.scope}`);
|
|
22
|
+
const perms = Object.entries(data.key.permissions).filter(([, v]) => v).map(([k]) => k);
|
|
23
|
+
console.log(`Perms: ${perms.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
whoami
|
|
28
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stow-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI for Stow file storage",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/howells/stow.git",
|
|
10
|
+
"directory": "packages/stow-cli"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://stow.sh",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"stow",
|
|
15
|
+
"file-storage",
|
|
16
|
+
"cli",
|
|
17
|
+
"upload"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"stow": "./dist/cli.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup src/cli.ts --format esm --shims",
|
|
27
|
+
"dev": "tsup src/cli.ts --format esm --shims --watch",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@howells/stow-server": "workspace:*",
|
|
33
|
+
"clipboardy": "^5.3.0",
|
|
34
|
+
"commander": "^14.0.3",
|
|
35
|
+
"ink": "^6.8.0",
|
|
36
|
+
"ink-select-input": "^6.2.0",
|
|
37
|
+
"ink-spinner": "^5.0.0",
|
|
38
|
+
"ink-text-input": "^6.0.0",
|
|
39
|
+
"open": "^11.0.0",
|
|
40
|
+
"react": "^19.2.4"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@stow/typescript-config": "workspace:*",
|
|
44
|
+
"@types/node": "^25.3.0",
|
|
45
|
+
"@types/react": "19.2.14",
|
|
46
|
+
"tsup": "^8.5.1",
|
|
47
|
+
"typescript": "^5.9.3",
|
|
48
|
+
"vitest": "^4.0.18"
|
|
49
|
+
}
|
|
50
|
+
}
|