stow-cli 2.1.0 → 2.2.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/LICENSE +21 -0
- package/dist/app-Q6EW7VSM.js +249 -0
- package/dist/backfill-EVZT7RH6.js +67 -0
- package/dist/buckets-ZHP3LBLC.js +137 -0
- package/dist/chunk-3BLL5SQJ.js +27 -0
- package/dist/chunk-FZGOTXTE.js +45 -0
- package/dist/chunk-P2BKGBQE.js +136 -0
- package/dist/chunk-PLZFHPLC.js +52 -0
- package/dist/cli.js +77 -52
- package/dist/delete-YEXSMG4I.js +34 -0
- package/dist/describe-J4ZMUXK7.js +79 -0
- package/dist/drops-5VIEW3XZ.js +39 -0
- package/dist/files-UQODXWNT.js +206 -0
- package/dist/health-RICGWQGN.js +61 -0
- package/dist/jobs-PTV2W5PJ.js +102 -0
- package/dist/jobs-ZWSEXNFA.js +90 -0
- package/dist/maintenance-ZJW2ES4L.js +79 -0
- package/dist/profiles-CHBGKQOE.js +53 -0
- package/dist/queues-EZ2VZGXQ.js +61 -0
- package/dist/search-TRTPX2SQ.js +135 -0
- package/dist/tags-VH44BGQL.js +90 -0
- package/dist/upload-OS6Q6LW5.js +126 -0
- package/dist/whoami-TVRKBM74.js +28 -0
- package/package.json +12 -12
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Stow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatBytes
|
|
3
|
+
} from "./chunk-FZGOTXTE.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,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
adminRequest
|
|
3
|
+
} from "./chunk-QF7PVPWQ.js";
|
|
4
|
+
import {
|
|
5
|
+
output
|
|
6
|
+
} from "./chunk-P2BKGBQE.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
|
+
output(
|
|
28
|
+
result,
|
|
29
|
+
() => {
|
|
30
|
+
const lines = [];
|
|
31
|
+
if (result.dryRun) {
|
|
32
|
+
lines.push(
|
|
33
|
+
`[dry run] ${result.remaining} files need ${type} processing`
|
|
34
|
+
);
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|
|
37
|
+
lines.push(`Enqueued: ${result.enqueued ?? 0}`);
|
|
38
|
+
lines.push(`Remaining: ${result.remaining}`);
|
|
39
|
+
if (result.errors && result.errors.length > 0) {
|
|
40
|
+
lines.push(`
|
|
41
|
+
Errors (${result.errors.length}):`);
|
|
42
|
+
for (const err of result.errors) {
|
|
43
|
+
lines.push(` ${err.fileId}: ${err.error}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (result.nextCursor) {
|
|
47
|
+
lines.push("\n(more files available -- run again to continue)");
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
},
|
|
51
|
+
{ json: options.json }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
async function backfillDimensions(options) {
|
|
55
|
+
await runBackfill("dimensions", options);
|
|
56
|
+
}
|
|
57
|
+
async function backfillColors(options) {
|
|
58
|
+
await runBackfill("colors", options);
|
|
59
|
+
}
|
|
60
|
+
async function backfillEmbeddings(options) {
|
|
61
|
+
await runBackfill("embeddings", options);
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
backfillColors,
|
|
65
|
+
backfillDimensions,
|
|
66
|
+
backfillEmbeddings
|
|
67
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseJsonInput
|
|
3
|
+
} from "./chunk-3BLL5SQJ.js";
|
|
4
|
+
import {
|
|
5
|
+
validateBucketName
|
|
6
|
+
} from "./chunk-PLZFHPLC.js";
|
|
7
|
+
import {
|
|
8
|
+
isJsonOutput,
|
|
9
|
+
output
|
|
10
|
+
} from "./chunk-P2BKGBQE.js";
|
|
11
|
+
import {
|
|
12
|
+
formatBytes,
|
|
13
|
+
formatTable
|
|
14
|
+
} from "./chunk-FZGOTXTE.js";
|
|
15
|
+
import {
|
|
16
|
+
createStow
|
|
17
|
+
} from "./chunk-5LU25QZK.js";
|
|
18
|
+
import "./chunk-TOADDO2F.js";
|
|
19
|
+
|
|
20
|
+
// src/commands/buckets.ts
|
|
21
|
+
async function listBuckets() {
|
|
22
|
+
const stow = createStow();
|
|
23
|
+
const data = await stow.listBuckets();
|
|
24
|
+
if (data.buckets.length === 0) {
|
|
25
|
+
if (isJsonOutput()) {
|
|
26
|
+
output(data);
|
|
27
|
+
} else {
|
|
28
|
+
console.log(
|
|
29
|
+
"No buckets yet. Create one with: stow buckets create <name>"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
output(data, () => {
|
|
35
|
+
const rows = data.buckets.map((b) => [
|
|
36
|
+
b.name,
|
|
37
|
+
b.isPublic ? "public" : "private",
|
|
38
|
+
b.searchable ? "yes" : "no",
|
|
39
|
+
`${b.fileCount ?? 0} files`,
|
|
40
|
+
formatBytes(b.usageBytes ?? 0),
|
|
41
|
+
b.description || ""
|
|
42
|
+
]);
|
|
43
|
+
return formatTable(
|
|
44
|
+
["Name", "Access", "Search", "Files", "Size", "Description"],
|
|
45
|
+
rows
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function createBucket(name, options) {
|
|
50
|
+
const input = parseJsonInput(options.inputJson, {
|
|
51
|
+
name,
|
|
52
|
+
description: options.description,
|
|
53
|
+
public: options.public
|
|
54
|
+
});
|
|
55
|
+
validateBucketName(input.name);
|
|
56
|
+
if (options.dryRun) {
|
|
57
|
+
console.log(
|
|
58
|
+
JSON.stringify(
|
|
59
|
+
{
|
|
60
|
+
dryRun: true,
|
|
61
|
+
action: "createBucket",
|
|
62
|
+
details: {
|
|
63
|
+
name: input.name,
|
|
64
|
+
description: input.description ?? null,
|
|
65
|
+
isPublic: input.public ?? false
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
null,
|
|
69
|
+
2
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const stow = createStow();
|
|
75
|
+
const bucket = await stow.createBucket({
|
|
76
|
+
name: input.name,
|
|
77
|
+
...input.description ? { description: input.description } : {},
|
|
78
|
+
...input.public ? { isPublic: true } : {}
|
|
79
|
+
});
|
|
80
|
+
console.log(`Created bucket: ${bucket.name}`);
|
|
81
|
+
}
|
|
82
|
+
async function renameBucket(name, newName, options) {
|
|
83
|
+
const input = parseJsonInput(
|
|
84
|
+
options.inputJson,
|
|
85
|
+
{ name, newName }
|
|
86
|
+
);
|
|
87
|
+
validateBucketName(input.name);
|
|
88
|
+
validateBucketName(input.newName);
|
|
89
|
+
if (options.dryRun) {
|
|
90
|
+
console.log(
|
|
91
|
+
JSON.stringify(
|
|
92
|
+
{
|
|
93
|
+
dryRun: true,
|
|
94
|
+
action: "renameBucket",
|
|
95
|
+
details: { name: input.name, newName: input.newName }
|
|
96
|
+
},
|
|
97
|
+
null,
|
|
98
|
+
2
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!options.yes) {
|
|
104
|
+
console.error(
|
|
105
|
+
"Warning: Renaming a bucket will break any existing URLs using the old name."
|
|
106
|
+
);
|
|
107
|
+
console.error("Use --yes to skip this warning.");
|
|
108
|
+
}
|
|
109
|
+
const stow = createStow();
|
|
110
|
+
const bucket = await stow.renameBucket(input.name, input.newName);
|
|
111
|
+
console.log(`Renamed bucket: ${input.name} \u2192 ${bucket.name}`);
|
|
112
|
+
}
|
|
113
|
+
async function deleteBucket(id, options = {}) {
|
|
114
|
+
if (options.dryRun) {
|
|
115
|
+
console.log(
|
|
116
|
+
JSON.stringify(
|
|
117
|
+
{
|
|
118
|
+
dryRun: true,
|
|
119
|
+
action: "deleteBucket",
|
|
120
|
+
details: { id }
|
|
121
|
+
},
|
|
122
|
+
null,
|
|
123
|
+
2
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const stow = createStow();
|
|
129
|
+
await stow.deleteBucket(id);
|
|
130
|
+
console.log(`Deleted bucket: ${id}`);
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
createBucket,
|
|
134
|
+
deleteBucket,
|
|
135
|
+
listBuckets,
|
|
136
|
+
renameBucket
|
|
137
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/lib/parse-json-input.ts
|
|
2
|
+
function parseJsonInput(jsonStr, flagValues) {
|
|
3
|
+
if (!jsonStr) return flagValues;
|
|
4
|
+
let parsed;
|
|
5
|
+
try {
|
|
6
|
+
parsed = JSON.parse(jsonStr);
|
|
7
|
+
} catch {
|
|
8
|
+
throw new Error(`Invalid JSON in --input-json: ${jsonStr.slice(0, 100)}`);
|
|
9
|
+
}
|
|
10
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
11
|
+
throw new Error("--input-json must be a JSON object");
|
|
12
|
+
}
|
|
13
|
+
return { ...parsed, ...stripUndefined(flagValues) };
|
|
14
|
+
}
|
|
15
|
+
function stripUndefined(obj) {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
18
|
+
if (value !== void 0) {
|
|
19
|
+
result[key] = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
parseJsonInput
|
|
27
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
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 usageBar(used, total, width = 20) {
|
|
34
|
+
const ratio = Math.min(used / total, 1);
|
|
35
|
+
const filled = Math.round(ratio * width);
|
|
36
|
+
const empty = width - filled;
|
|
37
|
+
const pct = Math.round(ratio * 100);
|
|
38
|
+
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}] ${pct}%`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
formatBytes,
|
|
43
|
+
formatTable,
|
|
44
|
+
usageBar
|
|
45
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/lib/sanitize-response.ts
|
|
2
|
+
var INJECTION_PATTERNS = [
|
|
3
|
+
// Direct instruction injection
|
|
4
|
+
/\b(?:ignore|disregard|forget)\b.*\b(?:previous|above|prior)\b.*\b(?:instructions?|rules?|context)\b/i,
|
|
5
|
+
// System prompt extraction attempts
|
|
6
|
+
/\b(?:reveal|show|print|output|display)\b.*\b(?:system\s*prompt|instructions?|rules?)\b/i,
|
|
7
|
+
// Role hijacking
|
|
8
|
+
/\byou\s+are\s+(?:now|a)\b/i,
|
|
9
|
+
// Tool/action injection
|
|
10
|
+
/\b(?:execute|run|call)\b.*\b(?:command|tool|function|bash|shell)\b/i,
|
|
11
|
+
// Markdown/XML injection that could affect agent parsing
|
|
12
|
+
/<\/?(?:system|user|assistant|tool_use|tool_result)\b/i
|
|
13
|
+
];
|
|
14
|
+
var USER_CONTENT_FIELDS = /* @__PURE__ */ new Set([
|
|
15
|
+
"originalFilename",
|
|
16
|
+
"filename",
|
|
17
|
+
"name",
|
|
18
|
+
"description",
|
|
19
|
+
"label",
|
|
20
|
+
"text",
|
|
21
|
+
"slug",
|
|
22
|
+
"webhookUrl"
|
|
23
|
+
]);
|
|
24
|
+
function detectInjection(value) {
|
|
25
|
+
return INJECTION_PATTERNS.some((pattern) => pattern.test(value));
|
|
26
|
+
}
|
|
27
|
+
function sanitizeValue(value) {
|
|
28
|
+
if (detectInjection(value)) {
|
|
29
|
+
return `[FLAGGED: potential prompt injection] ${value}`;
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function sanitizeResponse(data) {
|
|
34
|
+
if (data === null || data === void 0) return data;
|
|
35
|
+
if (typeof data === "string") {
|
|
36
|
+
return sanitizeValue(data);
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(data)) {
|
|
39
|
+
return data.map((item) => sanitizeResponse(item));
|
|
40
|
+
}
|
|
41
|
+
if (typeof data === "object") {
|
|
42
|
+
const result = {};
|
|
43
|
+
for (const [key, value] of Object.entries(
|
|
44
|
+
data
|
|
45
|
+
)) {
|
|
46
|
+
if (USER_CONTENT_FIELDS.has(key) && typeof value === "string") {
|
|
47
|
+
result[key] = sanitizeValue(value);
|
|
48
|
+
} else if (typeof value === "object" && value !== null) {
|
|
49
|
+
result[key] = sanitizeResponse(value);
|
|
50
|
+
} else {
|
|
51
|
+
result[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/lib/output.ts
|
|
60
|
+
var _forceHuman = false;
|
|
61
|
+
var _globalFields;
|
|
62
|
+
var _globalNdjson = false;
|
|
63
|
+
function setForceHuman(value) {
|
|
64
|
+
_forceHuman = value;
|
|
65
|
+
}
|
|
66
|
+
function setGlobalFields(fields) {
|
|
67
|
+
_globalFields = fields;
|
|
68
|
+
}
|
|
69
|
+
function setGlobalNdjson(value) {
|
|
70
|
+
_globalNdjson = value;
|
|
71
|
+
}
|
|
72
|
+
function isJsonOutput() {
|
|
73
|
+
if (_forceHuman) return false;
|
|
74
|
+
return !process.stdout.isTTY;
|
|
75
|
+
}
|
|
76
|
+
function output(data, humanFormatter, options) {
|
|
77
|
+
const sanitized = sanitizeResponse(data);
|
|
78
|
+
const masked = applyFieldMask(sanitized, _globalFields);
|
|
79
|
+
if (_globalNdjson && Array.isArray(masked)) {
|
|
80
|
+
outputNdjson(masked);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (options?.json || isJsonOutput()) {
|
|
84
|
+
console.log(JSON.stringify(masked, null, 2));
|
|
85
|
+
} else if (humanFormatter && !_globalFields) {
|
|
86
|
+
console.log(humanFormatter());
|
|
87
|
+
} else {
|
|
88
|
+
console.log(JSON.stringify(masked, null, 2));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function outputError(error, code, details) {
|
|
92
|
+
if (isJsonOutput()) {
|
|
93
|
+
console.error(
|
|
94
|
+
JSON.stringify({ error, ...code ? { code } : {}, ...details })
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
console.error(`Error: ${error}`);
|
|
98
|
+
}
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
function outputNdjson(items) {
|
|
102
|
+
for (const item of items) {
|
|
103
|
+
const sanitized = sanitizeResponse(item);
|
|
104
|
+
console.log(JSON.stringify(sanitized));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function applyFieldMask(data, fields) {
|
|
108
|
+
if (!fields) return data;
|
|
109
|
+
const fieldSet = new Set(fields.split(",").map((f) => f.trim()));
|
|
110
|
+
if (Array.isArray(data)) {
|
|
111
|
+
return data.map((item) => pickFields(item, fieldSet));
|
|
112
|
+
}
|
|
113
|
+
if (typeof data === "object" && data !== null) {
|
|
114
|
+
return pickFields(data, fieldSet);
|
|
115
|
+
}
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
function pickFields(obj, fieldSet) {
|
|
119
|
+
if (typeof obj !== "object" || obj === null) return {};
|
|
120
|
+
const result = {};
|
|
121
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
122
|
+
if (fieldSet.has(key)) {
|
|
123
|
+
result[key] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export {
|
|
130
|
+
setForceHuman,
|
|
131
|
+
setGlobalFields,
|
|
132
|
+
setGlobalNdjson,
|
|
133
|
+
isJsonOutput,
|
|
134
|
+
output,
|
|
135
|
+
outputError
|
|
136
|
+
};
|