stow-cli 1.0.2 → 2.0.1

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,249 @@
1
+ import {
2
+ formatBytes
3
+ } from "./chunk-ELSDWMEB.js";
4
+ import {
5
+ createStow
6
+ } from "./chunk-5LU25QZK.js";
7
+ import "./chunk-TOADDO2F.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,61 @@
1
+ import {
2
+ adminRequest
3
+ } from "./chunk-QF7PVPWQ.js";
4
+ import {
5
+ outputJson
6
+ } from "./chunk-ELSDWMEB.js";
7
+ import "./chunk-TOADDO2F.js";
8
+
9
+ // src/commands/admin/backfill.ts
10
+ async function runBackfill(type, options) {
11
+ const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null;
12
+ const body = {};
13
+ if (options.bucket) {
14
+ body.bucketId = options.bucket;
15
+ }
16
+ if (parsedLimit && parsedLimit > 0) {
17
+ body.limit = parsedLimit;
18
+ }
19
+ if (options.dryRun) {
20
+ body.dryRun = true;
21
+ }
22
+ const result = await adminRequest({
23
+ method: "POST",
24
+ path: `/internal/files/${type}/backfill`,
25
+ body
26
+ });
27
+ if (options.json) {
28
+ outputJson(result);
29
+ return;
30
+ }
31
+ if (result.dryRun) {
32
+ console.log(`[dry run] ${result.remaining} files need ${type} processing`);
33
+ return;
34
+ }
35
+ console.log(`Enqueued: ${result.enqueued ?? 0}`);
36
+ console.log(`Remaining: ${result.remaining}`);
37
+ if (result.errors && result.errors.length > 0) {
38
+ console.error(`
39
+ Errors (${result.errors.length}):`);
40
+ for (const err of result.errors) {
41
+ console.error(` ${err.fileId}: ${err.error}`);
42
+ }
43
+ }
44
+ if (result.nextCursor) {
45
+ console.log("\n(more files available \u2014 run again to continue)");
46
+ }
47
+ }
48
+ async function backfillDimensions(options) {
49
+ await runBackfill("dimensions", options);
50
+ }
51
+ async function backfillColors(options) {
52
+ await runBackfill("colors", options);
53
+ }
54
+ async function backfillEmbeddings(options) {
55
+ await runBackfill("embeddings", options);
56
+ }
57
+ export {
58
+ backfillColors,
59
+ backfillDimensions,
60
+ backfillEmbeddings
61
+ };
@@ -0,0 +1,63 @@
1
+ import {
2
+ formatBytes,
3
+ formatTable
4
+ } from "./chunk-ELSDWMEB.js";
5
+ import {
6
+ createStow
7
+ } from "./chunk-5LU25QZK.js";
8
+ import "./chunk-TOADDO2F.js";
9
+
10
+ // src/commands/buckets.ts
11
+ async function listBuckets() {
12
+ const stow = createStow();
13
+ const data = await stow.listBuckets();
14
+ if (data.buckets.length === 0) {
15
+ console.log("No buckets yet. Create one with: stow buckets create <name>");
16
+ return;
17
+ }
18
+ const rows = data.buckets.map((b) => [
19
+ b.name,
20
+ b.isPublic ? "public" : "private",
21
+ b.searchable ? "yes" : "no",
22
+ `${b.fileCount ?? 0} files`,
23
+ formatBytes(b.usageBytes ?? 0),
24
+ b.description || ""
25
+ ]);
26
+ console.log(
27
+ formatTable(
28
+ ["Name", "Access", "Search", "Files", "Size", "Description"],
29
+ rows
30
+ )
31
+ );
32
+ }
33
+ async function createBucket(name, options) {
34
+ const stow = createStow();
35
+ const bucket = await stow.createBucket({
36
+ name,
37
+ ...options.description ? { description: options.description } : {},
38
+ ...options.public ? { isPublic: true } : {}
39
+ });
40
+ console.log(`Created bucket: ${bucket.name}`);
41
+ }
42
+ async function renameBucket(name, newName, options) {
43
+ if (!options.yes) {
44
+ console.error(
45
+ "Warning: Renaming a bucket will break any existing URLs using the old name."
46
+ );
47
+ console.error("Use --yes to skip this warning.");
48
+ }
49
+ const stow = createStow();
50
+ const bucket = await stow.renameBucket(name, newName);
51
+ console.log(`Renamed bucket: ${name} \u2192 ${bucket.name}`);
52
+ }
53
+ async function deleteBucket(id) {
54
+ const stow = createStow();
55
+ await stow.deleteBucket(id);
56
+ console.log(`Deleted bucket: ${id}`);
57
+ }
58
+ export {
59
+ createBucket,
60
+ deleteBucket,
61
+ listBuckets,
62
+ renameBucket
63
+ };
@@ -0,0 +1,17 @@
1
+ import {
2
+ getApiKey,
3
+ getBaseUrl
4
+ } from "./chunk-TOADDO2F.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,49 @@
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] ?? 0)).join(" "));
25
+ lines.push(sep);
26
+ for (const row of rows) {
27
+ lines.push(
28
+ row.map((cell, i) => pad(cell || "", widths[i] ?? 0)).join(" ")
29
+ );
30
+ }
31
+ return lines.join("\n");
32
+ }
33
+ function outputJson(data) {
34
+ console.log(JSON.stringify(data, null, 2));
35
+ }
36
+ function usageBar(used, total, width = 20) {
37
+ const ratio = Math.min(used / total, 1);
38
+ const filled = Math.round(ratio * width);
39
+ const empty = width - filled;
40
+ const pct = Math.round(ratio * 100);
41
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}] ${pct}%`;
42
+ }
43
+
44
+ export {
45
+ formatBytes,
46
+ formatTable,
47
+ outputJson,
48
+ usageBar
49
+ };
@@ -0,0 +1,38 @@
1
+ import {
2
+ getBaseUrl
3
+ } from "./chunk-TOADDO2F.js";
4
+
5
+ // src/lib/admin-client.ts
6
+ function getAdminSecret() {
7
+ const secret = process.env.STOW_ADMIN_SECRET;
8
+ if (!secret) {
9
+ console.error("Error: STOW_ADMIN_SECRET environment variable not set");
10
+ console.error("");
11
+ console.error("Set your admin secret:");
12
+ console.error(" export STOW_ADMIN_SECRET=your-secret");
13
+ process.exit(1);
14
+ }
15
+ return secret;
16
+ }
17
+ async function adminRequest(opts) {
18
+ const baseUrl = getBaseUrl();
19
+ const secret = getAdminSecret();
20
+ const res = await fetch(`${baseUrl}${opts.path}`, {
21
+ method: opts.method,
22
+ headers: {
23
+ Authorization: `Bearer ${secret}`,
24
+ ...opts.body ? { "Content-Type": "application/json" } : {}
25
+ },
26
+ ...opts.body ? { body: JSON.stringify(opts.body) } : {}
27
+ });
28
+ if (!res.ok) {
29
+ const body = await res.json().catch(() => ({ error: res.statusText }));
30
+ const message = body.error ?? `HTTP ${res.status}`;
31
+ throw new Error(message);
32
+ }
33
+ return await res.json();
34
+ }
35
+
36
+ export {
37
+ adminRequest
38
+ };
@@ -0,0 +1,23 @@
1
+ // src/lib/config.ts
2
+ var DEFAULT_BASE_URL = "https://api.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
+ };