| import { | ||
| formatBytes | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/interactive/app.tsx | ||
| import { render } from "ink"; | ||
| import { useEffect as useEffect3, useState as useState3 } from "react"; | ||
| // src/interactive/components/bucket-list.tsx | ||
| import { Box as Box3, Text as Text3, useApp, useInput } from "ink"; | ||
| import Spinner from "ink-spinner"; | ||
| import { useEffect, useState } from "react"; | ||
| // src/interactive/components/header.tsx | ||
| import { Box, Text } from "ink"; | ||
| import { jsx, jsxs } from "react/jsx-runtime"; | ||
| function Header({ path, email, size }) { | ||
| return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [ | ||
| /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", width: "100%", children: [ | ||
| /* @__PURE__ */ jsxs(Text, { bold: true, children: [ | ||
| "STOW", | ||
| path ? ` > ${path}` : "" | ||
| ] }), | ||
| /* @__PURE__ */ jsx(Text, { dimColor: true, children: size || email || "" }) | ||
| ] }), | ||
| /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) }) | ||
| ] }); | ||
| } | ||
| // src/interactive/components/status-bar.tsx | ||
| import { Box as Box2, Text as Text2 } from "ink"; | ||
| import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime"; | ||
| function StatusBar({ hints }) { | ||
| return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: hints.map((h, i) => /* @__PURE__ */ jsx2(Box2, { marginRight: 2, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [ | ||
| "[", | ||
| h.key, | ||
| "]", | ||
| h.label, | ||
| i < hints.length - 1 ? "" : "" | ||
| ] }) }, h.key)) }); | ||
| } | ||
| // src/interactive/components/bucket-list.tsx | ||
| import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime"; | ||
| function toBucket(result) { | ||
| return { | ||
| id: result.id, | ||
| name: result.name, | ||
| description: result.description ?? null, | ||
| fileCount: result.fileCount ?? 0, | ||
| isPublic: result.isPublic ?? false, | ||
| usageBytes: result.usageBytes ?? 0 | ||
| }; | ||
| } | ||
| function BucketList({ onSelect, email }) { | ||
| const { exit } = useApp(); | ||
| const [buckets, setBuckets] = useState([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [errorMessage, setErrorMessage] = useState(null); | ||
| const [cursor, setCursor] = useState(0); | ||
| useEffect(() => { | ||
| async function loadBuckets() { | ||
| try { | ||
| const stow = createStow(); | ||
| const data = await stow.listBuckets(); | ||
| setBuckets(data.buckets.map(toBucket)); | ||
| } catch (error) { | ||
| setErrorMessage(error instanceof Error ? error.message : String(error)); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| loadBuckets(); | ||
| }, []); | ||
| useInput((input, key) => { | ||
| if (input === "q") { | ||
| exit(); | ||
| return; | ||
| } | ||
| if (key.upArrow) { | ||
| setCursor((c) => Math.max(0, c - 1)); | ||
| } else if (key.downArrow) { | ||
| setCursor((c) => Math.min(buckets.length - 1, c + 1)); | ||
| } else if (key.return && buckets.length > 0) { | ||
| onSelect(buckets[cursor]); | ||
| } | ||
| }); | ||
| if (loading) { | ||
| return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { children: [ | ||
| /* @__PURE__ */ jsx3(Spinner, { type: "dots" }), | ||
| " Loading buckets..." | ||
| ] }) }); | ||
| } | ||
| if (errorMessage) { | ||
| return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [ | ||
| "Error: ", | ||
| errorMessage | ||
| ] }) }); | ||
| } | ||
| return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [ | ||
| /* @__PURE__ */ jsx3(Header, { email }), | ||
| 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: [ | ||
| i === cursor ? "\u25B8 " : " ", | ||
| b.name.padEnd(20), | ||
| `${b.fileCount} files`.padEnd(14), | ||
| formatBytes(b.usageBytes) | ||
| ] }) }, b.id)), | ||
| /* @__PURE__ */ jsx3( | ||
| StatusBar, | ||
| { | ||
| hints: [ | ||
| { key: "\u2191\u2193", label: "navigate" }, | ||
| { key: "\u21B5", label: "open" }, | ||
| { key: "q", label: "uit" } | ||
| ] | ||
| } | ||
| ) | ||
| ] }); | ||
| } | ||
| // src/interactive/components/file-list.tsx | ||
| import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink"; | ||
| import Spinner2 from "ink-spinner"; | ||
| import { useEffect as useEffect2, useState as useState2 } from "react"; | ||
| import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime"; | ||
| function FileList({ bucket, onBack }) { | ||
| const [files, setFiles] = useState2([]); | ||
| const [loading, setLoading] = useState2(true); | ||
| const [errorMessage, setErrorMessage] = useState2(null); | ||
| const [cursor, setCursor] = useState2(0); | ||
| const [nextCursor, setNextCursor] = useState2(null); | ||
| const [copied, setCopied] = useState2(false); | ||
| async function loadFiles(pageCursor) { | ||
| setLoading(true); | ||
| try { | ||
| const stow = createStow(); | ||
| const data = await stow.listFiles({ | ||
| bucket: bucket.name, | ||
| ...pageCursor ? { cursor: pageCursor } : {} | ||
| }); | ||
| setFiles((prev) => pageCursor ? [...prev, ...data.files] : data.files); | ||
| setNextCursor(data.nextCursor); | ||
| } catch (error) { | ||
| setErrorMessage(error instanceof Error ? error.message : String(error)); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| useEffect2(() => { | ||
| loadFiles(); | ||
| }, []); | ||
| async function copyCurrentFileUrl() { | ||
| const currentFile = files[cursor]; | ||
| if (!currentFile) { | ||
| return; | ||
| } | ||
| if (!currentFile.url) { | ||
| setErrorMessage("Selected file has no URL"); | ||
| return; | ||
| } | ||
| const { default: clipboard } = await import("clipboardy"); | ||
| await clipboard.write(currentFile.url); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2e3); | ||
| } | ||
| useInput2((input, key) => { | ||
| if (key.escape || key.backspace || key.leftArrow && !loading) { | ||
| onBack(); | ||
| return; | ||
| } | ||
| if (key.upArrow) { | ||
| setCursor((c) => Math.max(0, c - 1)); | ||
| } else if (key.downArrow) { | ||
| setCursor((c) => { | ||
| const next = Math.min(files.length - 1, c + 1); | ||
| if (next >= files.length - 3 && nextCursor && !loading) { | ||
| loadFiles(nextCursor); | ||
| } | ||
| return next; | ||
| }); | ||
| } else if (input === "c" && files.length > 0) { | ||
| copyCurrentFileUrl(); | ||
| } | ||
| }); | ||
| if (errorMessage) { | ||
| return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [ | ||
| /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [ | ||
| "Error: ", | ||
| errorMessage | ||
| ] }), | ||
| /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press Esc to go back." }) | ||
| ] }); | ||
| } | ||
| return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [ | ||
| /* @__PURE__ */ jsx4(Header, { path: bucket.name, size: formatBytes(bucket.usageBytes) }), | ||
| loading && files.length === 0 && /* @__PURE__ */ jsxs4(Text4, { children: [ | ||
| /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }), | ||
| " Loading files..." | ||
| ] }), | ||
| !loading && files.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No files in this bucket." }), | ||
| files.length > 0 && files.map((f, i) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color: i === cursor ? "cyan" : void 0, children: [ | ||
| i === cursor ? "\u25B8 " : " ", | ||
| f.key.padEnd(28), | ||
| formatBytes(f.size).padEnd(10), | ||
| f.lastModified.split("T")[0] | ||
| ] }) }, f.key)), | ||
| loading && files.length > 0 && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [ | ||
| /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }), | ||
| " Loading more..." | ||
| ] }), | ||
| copied && /* @__PURE__ */ jsx4(Text4, { color: "green", children: "Copied URL to clipboard!" }), | ||
| /* @__PURE__ */ jsx4( | ||
| StatusBar, | ||
| { | ||
| hints: [ | ||
| { key: "\u2191\u2193", label: "navigate" }, | ||
| { key: "c", label: "opy url" }, | ||
| { key: "\u2190", label: "back" } | ||
| ] | ||
| } | ||
| ) | ||
| ] }); | ||
| } | ||
| // src/interactive/app.tsx | ||
| import { jsx as jsx5 } from "react/jsx-runtime"; | ||
| function App() { | ||
| const [screen, setScreen] = useState3({ type: "buckets" }); | ||
| const [email, setEmail] = useState3(); | ||
| useEffect3(() => { | ||
| async function loadEmail() { | ||
| const stow = createStow(); | ||
| try { | ||
| const data = await stow.whoami(); | ||
| setEmail(data.user.email); | ||
| } catch { | ||
| } | ||
| } | ||
| loadEmail(); | ||
| }, []); | ||
| if (screen.type === "files") { | ||
| return /* @__PURE__ */ jsx5(FileList, { bucket: screen.bucket, onBack: () => setScreen({ type: "buckets" }) }); | ||
| } | ||
| return /* @__PURE__ */ jsx5(BucketList, { email, onSelect: (bucket) => setScreen({ type: "files", bucket }) }); | ||
| } | ||
| function startInteractive() { | ||
| render(/* @__PURE__ */ jsx5(App, {})); | ||
| } | ||
| export { | ||
| startInteractive | ||
| }; |
| import { | ||
| adminRequest | ||
| } from "./chunk-QF7PVPWQ.js"; | ||
| import { | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/admin/backfill.ts | ||
| async function runBackfill(type, options) { | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const body = {}; | ||
| if (options.bucket) { | ||
| body.bucketId = options.bucket; | ||
| } | ||
| if (parsedLimit && parsedLimit > 0) { | ||
| body.limit = parsedLimit; | ||
| } | ||
| if (options.dryRun) { | ||
| body.dryRun = true; | ||
| } | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/internal/files/${type}/backfill`, | ||
| body | ||
| }); | ||
| output( | ||
| result, | ||
| () => { | ||
| const lines = []; | ||
| if (result.dryRun) { | ||
| lines.push(`[dry run] ${result.remaining} files need ${type} processing`); | ||
| return lines.join("\n"); | ||
| } | ||
| lines.push(`Enqueued: ${result.enqueued ?? 0}`); | ||
| lines.push(`Remaining: ${result.remaining}`); | ||
| if (result.errors && result.errors.length > 0) { | ||
| lines.push(` | ||
| Errors (${result.errors.length}):`); | ||
| for (const err of result.errors) { | ||
| lines.push(` ${err.fileId}: ${err.error}`); | ||
| } | ||
| } | ||
| if (result.nextCursor) { | ||
| lines.push("\n(more files available -- run again to continue)"); | ||
| } | ||
| return lines.join("\n"); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function backfillDimensions(options) { | ||
| await runBackfill("dimensions", options); | ||
| } | ||
| async function backfillColors(options) { | ||
| await runBackfill("colors", options); | ||
| } | ||
| async function backfillEmbeddings(options) { | ||
| await runBackfill("embeddings", options); | ||
| } | ||
| export { | ||
| backfillColors, | ||
| backfillDimensions, | ||
| backfillEmbeddings | ||
| }; |
| import { | ||
| adminRequest | ||
| } from "./chunk-QF7PVPWQ.js"; | ||
| import { | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/admin/backfill.ts | ||
| async function runBackfill(type, options) { | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const body = {}; | ||
| if (options.bucket) { | ||
| body.bucketId = options.bucket; | ||
| } | ||
| if (parsedLimit && parsedLimit > 0) { | ||
| body.limit = parsedLimit; | ||
| } | ||
| if (options.dryRun) { | ||
| body.dryRun = true; | ||
| } | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/internal/files/${type}/backfill`, | ||
| body | ||
| }); | ||
| output( | ||
| result, | ||
| () => { | ||
| const lines = []; | ||
| if (result.dryRun) { | ||
| lines.push( | ||
| `[dry run] ${result.remaining} files need ${type} processing` | ||
| ); | ||
| return lines.join("\n"); | ||
| } | ||
| lines.push(`Enqueued: ${result.enqueued ?? 0}`); | ||
| lines.push(`Remaining: ${result.remaining}`); | ||
| if (result.errors && result.errors.length > 0) { | ||
| lines.push(` | ||
| Errors (${result.errors.length}):`); | ||
| for (const err of result.errors) { | ||
| lines.push(` ${err.fileId}: ${err.error}`); | ||
| } | ||
| } | ||
| if (result.nextCursor) { | ||
| lines.push("\n(more files available -- run again to continue)"); | ||
| } | ||
| return lines.join("\n"); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function backfillDimensions(options) { | ||
| await runBackfill("dimensions", options); | ||
| } | ||
| async function backfillColors(options) { | ||
| await runBackfill("colors", options); | ||
| } | ||
| async function backfillEmbeddings(options) { | ||
| await runBackfill("embeddings", options); | ||
| } | ||
| export { | ||
| backfillColors, | ||
| backfillDimensions, | ||
| backfillEmbeddings | ||
| }; |
| import { | ||
| parseJsonInput | ||
| } from "./chunk-XVKIRHTX.js"; | ||
| import { | ||
| validateBucketName | ||
| } from "./chunk-NBHBVKP5.js"; | ||
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import { | ||
| formatBytes, | ||
| formatTable | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/buckets.ts | ||
| async function listBuckets() { | ||
| const stow = createStow(); | ||
| const data = await stow.listBuckets(); | ||
| if (data.buckets.length === 0) { | ||
| if (isJsonOutput()) { | ||
| output(data); | ||
| } else { | ||
| console.log("No buckets yet. Create one with: stow buckets create <name>"); | ||
| } | ||
| return; | ||
| } | ||
| output(data, () => { | ||
| const rows = data.buckets.map((b) => [ | ||
| b.name, | ||
| b.isPublic ? "public" : "private", | ||
| b.searchable ? "yes" : "no", | ||
| `${b.fileCount ?? 0} files`, | ||
| formatBytes(b.usageBytes ?? 0), | ||
| b.description || "" | ||
| ]); | ||
| return formatTable(["Name", "Access", "Search", "Files", "Size", "Description"], rows); | ||
| }); | ||
| } | ||
| async function createBucket(name, options) { | ||
| const input = parseJsonInput(options.inputJson, { | ||
| name, | ||
| description: options.description, | ||
| public: options.public | ||
| }); | ||
| validateBucketName(input.name); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "createBucket", | ||
| details: { | ||
| name: input.name, | ||
| description: input.description ?? null, | ||
| isPublic: input.public ?? false | ||
| } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| const bucket = await stow.createBucket({ | ||
| name: input.name, | ||
| ...input.description ? { description: input.description } : {}, | ||
| ...input.public ? { isPublic: true } : {} | ||
| }); | ||
| console.log(`Created bucket: ${bucket.name}`); | ||
| } | ||
| async function renameBucket(name, newName, options) { | ||
| const input = parseJsonInput(options.inputJson, { | ||
| name, | ||
| newName | ||
| }); | ||
| validateBucketName(input.name); | ||
| validateBucketName(input.newName); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "renameBucket", | ||
| details: { name: input.name, newName: input.newName } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| if (!options.yes) { | ||
| console.error("Warning: Renaming a bucket will break any existing URLs using the old name."); | ||
| console.error("Use --yes to skip this warning."); | ||
| } | ||
| const stow = createStow(); | ||
| const bucket = await stow.renameBucket(input.name, input.newName); | ||
| console.log(`Renamed bucket: ${input.name} \u2192 ${bucket.name}`); | ||
| } | ||
| async function deleteBucket(id, options = {}) { | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "deleteBucket", | ||
| details: { id } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| await stow.deleteBucket(id); | ||
| console.log(`Deleted bucket: ${id}`); | ||
| } | ||
| export { | ||
| createBucket, | ||
| deleteBucket, | ||
| listBuckets, | ||
| renameBucket | ||
| }; |
| import { | ||
| parseJsonInput | ||
| } from "./chunk-AHBVZRDR.js"; | ||
| import { | ||
| validateBucketName | ||
| } from "./chunk-533UGNLM.js"; | ||
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import { | ||
| formatBytes, | ||
| formatTable | ||
| } from "./chunk-FZGOTXTE.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/buckets.ts | ||
| async function listBuckets() { | ||
| const stow = createStow(); | ||
| const data = await stow.listBuckets(); | ||
| if (data.buckets.length === 0) { | ||
| if (isJsonOutput()) { | ||
| output(data); | ||
| } else { | ||
| console.log( | ||
| "No buckets yet. Create one with: stow buckets create <name>" | ||
| ); | ||
| } | ||
| return; | ||
| } | ||
| output(data, () => { | ||
| const rows = data.buckets.map((b) => [ | ||
| b.name, | ||
| b.isPublic ? "public" : "private", | ||
| b.searchable ? "yes" : "no", | ||
| `${b.fileCount ?? 0} files`, | ||
| formatBytes(b.usageBytes ?? 0), | ||
| b.description || "" | ||
| ]); | ||
| return formatTable( | ||
| ["Name", "Access", "Search", "Files", "Size", "Description"], | ||
| rows | ||
| ); | ||
| }); | ||
| } | ||
| async function createBucket(name, options) { | ||
| const input = parseJsonInput(options.inputJson, { | ||
| name, | ||
| description: options.description, | ||
| public: options.public | ||
| }); | ||
| validateBucketName(input.name); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "createBucket", | ||
| details: { | ||
| name: input.name, | ||
| description: input.description ?? null, | ||
| isPublic: input.public ?? false | ||
| } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| const bucket = await stow.createBucket({ | ||
| name: input.name, | ||
| ...input.description ? { description: input.description } : {}, | ||
| ...input.public ? { isPublic: true } : {} | ||
| }); | ||
| console.log(`Created bucket: ${bucket.name}`); | ||
| } | ||
| async function renameBucket(name, newName, options) { | ||
| const input = parseJsonInput( | ||
| options.inputJson, | ||
| { name, newName } | ||
| ); | ||
| validateBucketName(input.name); | ||
| validateBucketName(input.newName); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "renameBucket", | ||
| details: { name: input.name, newName: input.newName } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| if (!options.yes) { | ||
| console.error( | ||
| "Warning: Renaming a bucket will break any existing URLs using the old name." | ||
| ); | ||
| console.error("Use --yes to skip this warning."); | ||
| } | ||
| const stow = createStow(); | ||
| const bucket = await stow.renameBucket(input.name, input.newName); | ||
| console.log(`Renamed bucket: ${input.name} \u2192 ${bucket.name}`); | ||
| } | ||
| async function deleteBucket(id, options = {}) { | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "deleteBucket", | ||
| details: { id } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| await stow.deleteBucket(id); | ||
| console.log(`Deleted bucket: ${id}`); | ||
| } | ||
| export { | ||
| createBucket, | ||
| deleteBucket, | ||
| listBuckets, | ||
| renameBucket | ||
| }; |
| // src/lib/validate-input.ts | ||
| import path from "path"; | ||
| var InputValidationError = class extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = "InputValidationError"; | ||
| } | ||
| }; | ||
| function validateInput(value, context) { | ||
| if (value.includes("../") || value.includes("..\\")) { | ||
| throw new InputValidationError(`${context}: path traversal not allowed`); | ||
| } | ||
| if (context !== "url" && value.includes("?")) { | ||
| throw new InputValidationError(`${context}: embedded query parameters not allowed`); | ||
| } | ||
| if (/[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(value)) { | ||
| throw new InputValidationError(`${context}: control characters not allowed`); | ||
| } | ||
| if (/%25/.test(value)) { | ||
| throw new InputValidationError(`${context}: double-encoded values not allowed`); | ||
| } | ||
| if (/%2[fF]/.test(value) || /%2[eE]/.test(value)) { | ||
| throw new InputValidationError(`${context}: percent-encoded path characters not allowed`); | ||
| } | ||
| if (context !== "url" && value.includes("#")) { | ||
| throw new InputValidationError(`${context}: embedded hash fragments not allowed`); | ||
| } | ||
| return value; | ||
| } | ||
| function validateBucketName(name) { | ||
| return validateInput(name, "bucket name"); | ||
| } | ||
| function validateFileKey(key) { | ||
| return validateInput(key, "file key"); | ||
| } | ||
| export { | ||
| InputValidationError, | ||
| validateInput, | ||
| validateBucketName, | ||
| validateFileKey | ||
| }; |
| // src/lib/parse-json-input.ts | ||
| function parseJsonInput(jsonStr, flagValues) { | ||
| if (!jsonStr) { | ||
| return flagValues; | ||
| } | ||
| let parsed; | ||
| try { | ||
| parsed = JSON.parse(jsonStr); | ||
| } catch { | ||
| throw new Error(`Invalid JSON in --input-json: ${jsonStr.slice(0, 100)}`); | ||
| } | ||
| if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { | ||
| throw new Error("--input-json must be a JSON object"); | ||
| } | ||
| return { ...parsed, ...stripUndefined(flagValues) }; | ||
| } | ||
| function stripUndefined(obj) { | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| if (value !== void 0) { | ||
| result[key] = value; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| export { | ||
| parseJsonInput | ||
| }; |
| // src/lib/sanitize-response.ts | ||
| var INJECTION_PATTERNS = [ | ||
| // Direct instruction injection | ||
| /\b(?:ignore|disregard|forget)\b.*\b(?:previous|above|prior)\b.*\b(?:instructions?|rules?|context)\b/i, | ||
| // System prompt extraction attempts | ||
| /\b(?:reveal|show|print|output|display)\b.*\b(?:system\s*prompt|instructions?|rules?)\b/i, | ||
| // Role hijacking | ||
| /\byou\s+are\s+(?:now|a)\b/i, | ||
| // Tool/action injection | ||
| /\b(?:execute|run|call)\b.*\b(?:command|tool|function|bash|shell)\b/i, | ||
| // Markdown/XML injection that could affect agent parsing | ||
| /<\/?(?:system|user|assistant|tool_use|tool_result)\b/i | ||
| ]; | ||
| var USER_CONTENT_FIELDS = /* @__PURE__ */ new Set([ | ||
| "originalFilename", | ||
| "filename", | ||
| "name", | ||
| "description", | ||
| "label", | ||
| "text", | ||
| "slug", | ||
| "webhookUrl" | ||
| ]); | ||
| function detectInjection(value) { | ||
| return INJECTION_PATTERNS.some((pattern) => pattern.test(value)); | ||
| } | ||
| function sanitizeValue(value) { | ||
| if (detectInjection(value)) { | ||
| return `[FLAGGED: potential prompt injection] ${value}`; | ||
| } | ||
| return value; | ||
| } | ||
| function sanitizeResponse(data) { | ||
| if (data === null || data === void 0) { | ||
| return data; | ||
| } | ||
| if (typeof data === "string") { | ||
| return sanitizeValue(data); | ||
| } | ||
| if (Array.isArray(data)) { | ||
| return data.map((item) => sanitizeResponse(item)); | ||
| } | ||
| if (typeof data === "object") { | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(data)) { | ||
| if (USER_CONTENT_FIELDS.has(key) && typeof value === "string") { | ||
| result[key] = sanitizeValue(value); | ||
| } else if (typeof value === "object" && value !== null) { | ||
| result[key] = sanitizeResponse(value); | ||
| } else { | ||
| result[key] = value; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| return data; | ||
| } | ||
| // src/lib/output.ts | ||
| var _forceHuman = false; | ||
| var _globalFields; | ||
| var _globalNdjson = false; | ||
| function setForceHuman(value) { | ||
| _forceHuman = value; | ||
| } | ||
| function setGlobalFields(fields) { | ||
| _globalFields = fields; | ||
| } | ||
| function setGlobalNdjson(value) { | ||
| _globalNdjson = value; | ||
| } | ||
| function isJsonOutput() { | ||
| if (_forceHuman) { | ||
| return false; | ||
| } | ||
| return !process.stdout.isTTY; | ||
| } | ||
| function unwrapArray(data) { | ||
| if (typeof data !== "object" || data === null || Array.isArray(data)) { | ||
| return data; | ||
| } | ||
| const entries = Object.entries(data); | ||
| if (entries.length === 1 && Array.isArray(entries[0][1])) { | ||
| return entries[0][1]; | ||
| } | ||
| return data; | ||
| } | ||
| function pickFields(obj, fieldSet) { | ||
| if (typeof obj !== "object" || obj === null) { | ||
| return {}; | ||
| } | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| if (fieldSet.has(key)) { | ||
| result[key] = value; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| function applyFieldMask(data, fields) { | ||
| if (!fields) { | ||
| return data; | ||
| } | ||
| const fieldSet = new Set(fields.split(",").map((f) => f.trim())); | ||
| if (Array.isArray(data)) { | ||
| return data.map((item) => pickFields(item, fieldSet)); | ||
| } | ||
| if (typeof data === "object" && data !== null) { | ||
| return pickFields(data, fieldSet); | ||
| } | ||
| return data; | ||
| } | ||
| function outputNdjson(items) { | ||
| for (const item of items) { | ||
| const sanitized = sanitizeResponse(item); | ||
| console.log(JSON.stringify(sanitized)); | ||
| } | ||
| } | ||
| function output(data, humanFormatter, options) { | ||
| const sanitized = sanitizeResponse(data); | ||
| const unwrapped = _globalFields || _globalNdjson ? unwrapArray(sanitized) : sanitized; | ||
| const masked = applyFieldMask(unwrapped, _globalFields); | ||
| if (_globalNdjson && Array.isArray(masked)) { | ||
| outputNdjson(masked); | ||
| return; | ||
| } | ||
| if (options?.json || isJsonOutput()) { | ||
| console.log(JSON.stringify(masked, null, 2)); | ||
| } else if (humanFormatter && !_globalFields) { | ||
| console.log(humanFormatter()); | ||
| } else { | ||
| console.log(JSON.stringify(masked, null, 2)); | ||
| } | ||
| } | ||
| function outputError(error, code, details) { | ||
| if (isJsonOutput()) { | ||
| console.error(JSON.stringify({ error, ...code ? { code } : {}, ...details })); | ||
| } else { | ||
| console.error(`Error: ${error}`); | ||
| } | ||
| process.exit(1); | ||
| } | ||
| export { | ||
| setForceHuman, | ||
| setGlobalFields, | ||
| setGlobalNdjson, | ||
| isJsonOutput, | ||
| output, | ||
| outputError | ||
| }; |
| // src/lib/cli-docs.ts | ||
| var CLI_DOCS = { | ||
| root: { | ||
| description: "CLI for Stow file storage", | ||
| usage: "stow [command] [options]", | ||
| examples: [ | ||
| "stow whoami", | ||
| "stow upload ./photo.jpg --bucket photos", | ||
| "stow drop ./screenshot.png", | ||
| "stow buckets", | ||
| "stow files photos --limit 50", | ||
| "stow search text 'sunset beach'", | ||
| "stow admin health" | ||
| ], | ||
| notes: [ | ||
| "Set STOW_API_KEY before running commands that call the API.", | ||
| "Set STOW_API_URL to target a non-default environment.", | ||
| "Set STOW_ADMIN_SECRET for admin commands." | ||
| ] | ||
| }, | ||
| drop: { | ||
| description: "Upload a file and get a short URL (quick share)", | ||
| usage: "stow drop <file> [options]", | ||
| examples: ["stow drop ./video.mp4", "stow drop ./notes.txt --quiet"] | ||
| }, | ||
| upload: { | ||
| description: "Upload a file to a bucket", | ||
| usage: "stow upload <file> [options]", | ||
| examples: ["stow upload ./logo.png --bucket brand-assets", "stow upload ./clip.mov --quiet"] | ||
| }, | ||
| buckets: { | ||
| description: "List your buckets", | ||
| usage: "stow buckets", | ||
| examples: ["stow buckets"] | ||
| }, | ||
| bucketsCreate: { | ||
| description: "Create a new bucket", | ||
| usage: "stow buckets create <name> [options]", | ||
| examples: [ | ||
| "stow buckets create photos", | ||
| 'stow buckets create docs --description "Product docs"', | ||
| "stow buckets create public-media --public" | ||
| ] | ||
| }, | ||
| bucketsRename: { | ||
| description: "Rename a bucket", | ||
| usage: "stow buckets rename <name> <new-name> [options]", | ||
| examples: ["stow buckets rename old-name new-name --yes"], | ||
| notes: ["Renaming a bucket can break existing public URLs."] | ||
| }, | ||
| bucketsDelete: { | ||
| description: "Delete a bucket by ID", | ||
| usage: "stow buckets delete <id>", | ||
| examples: ["stow buckets delete 8f3d1ab4-..."] | ||
| }, | ||
| files: { | ||
| description: "List files in a bucket", | ||
| usage: "stow files <bucket> [options]", | ||
| examples: [ | ||
| "stow files photos", | ||
| "stow files photos --search avatars/ --limit 100", | ||
| "stow files photos --json" | ||
| ] | ||
| }, | ||
| filesGet: { | ||
| description: "Get details for a single file", | ||
| usage: "stow files get <bucket> <key>", | ||
| examples: ["stow files get photos hero.png", "stow files get photos hero.png --json"] | ||
| }, | ||
| filesUpdate: { | ||
| description: "Update file metadata", | ||
| usage: "stow files update <bucket> <key> -m key=value", | ||
| examples: [ | ||
| "stow files update photos hero.png -m alt='Hero image'", | ||
| "stow files update photos hero.png -m category=banner -m priority=high" | ||
| ] | ||
| }, | ||
| filesMissing: { | ||
| description: "List files missing processing data", | ||
| usage: "stow files missing <bucket> <type>", | ||
| examples: [ | ||
| "stow files missing brera dimensions", | ||
| "stow files missing brera embeddings --limit 200", | ||
| "stow files missing brera colors --json" | ||
| ], | ||
| notes: ["Valid types: dimensions, embeddings, colors"] | ||
| }, | ||
| filesEnrich: { | ||
| description: "Generate title, description, and alt text for an image", | ||
| usage: "stow files enrich <bucket> <key>", | ||
| examples: ["stow files enrich photos hero.jpg", "stow files enrich next l5igro4iutep3"], | ||
| notes: [ | ||
| "Triggers title, description, and alt text generation in parallel.", | ||
| "Requires a searchable bucket with image files." | ||
| ] | ||
| }, | ||
| drops: { | ||
| description: "List your drops with usage info", | ||
| usage: "stow drops", | ||
| examples: ["stow drops"] | ||
| }, | ||
| dropsDelete: { | ||
| description: "Delete a drop by ID", | ||
| usage: "stow drops delete <id>", | ||
| examples: ["stow drops delete drop_abc123"] | ||
| }, | ||
| delete: { | ||
| description: "Delete a file from a bucket", | ||
| usage: "stow delete <bucket> <key>", | ||
| examples: ["stow delete photos hero/banner.png"] | ||
| }, | ||
| whoami: { | ||
| description: "Show account info, usage stats, and API key details", | ||
| usage: "stow whoami", | ||
| examples: ["stow whoami"] | ||
| }, | ||
| open: { | ||
| description: "Open a bucket in the browser", | ||
| usage: "stow open <bucket>", | ||
| examples: ["stow open photos"] | ||
| }, | ||
| interactive: { | ||
| description: "Launch interactive TUI mode", | ||
| usage: "stow --interactive", | ||
| examples: ["stow", "stow --interactive"] | ||
| }, | ||
| search: { | ||
| description: "Search files across buckets", | ||
| usage: "stow search <subcommand>", | ||
| examples: [ | ||
| "stow search text 'sunset beach' -b photos", | ||
| "stow search similar --file hero.png -b photos", | ||
| 'stow search color --hex "#ff0000" -b photos', | ||
| "stow search diverse -b photos" | ||
| ] | ||
| }, | ||
| searchText: { | ||
| description: "Semantic text search", | ||
| usage: "stow search text <query> [options]", | ||
| examples: ["stow search text 'sunset beach' -b photos --limit 10 --json"] | ||
| }, | ||
| searchSimilar: { | ||
| description: "Find files similar to a given file", | ||
| usage: "stow search similar --file <key> [options]", | ||
| examples: ["stow search similar --file hero.png -b photos"] | ||
| }, | ||
| searchColor: { | ||
| description: "Search by color", | ||
| usage: 'stow search color --hex "#ff0000" [options]', | ||
| examples: ['stow search color --hex "#ff0000" -b photos --limit 20'] | ||
| }, | ||
| searchDiverse: { | ||
| description: "Diversity-aware search", | ||
| usage: "stow search diverse [options]", | ||
| examples: ["stow search diverse -b photos --limit 20"] | ||
| }, | ||
| tags: { | ||
| description: "Manage tags", | ||
| usage: "stow tags", | ||
| examples: ["stow tags", "stow tags create 'Hero Images'", "stow tags delete <id>"] | ||
| }, | ||
| tagsCreate: { | ||
| description: "Create a new tag", | ||
| usage: "stow tags create <name> [options]", | ||
| examples: ['stow tags create "Hero Images"', 'stow tags create "Featured" --color "#ff6600"'] | ||
| }, | ||
| tagsDelete: { | ||
| description: "Delete a tag by ID", | ||
| usage: "stow tags delete <id>", | ||
| examples: ["stow tags delete tag_abc123"] | ||
| }, | ||
| profiles: { | ||
| description: "Manage taste profiles", | ||
| usage: "stow profiles <subcommand>", | ||
| examples: [ | ||
| 'stow profiles create --name "My Profile"', | ||
| "stow profiles get <id>", | ||
| "stow profiles delete <id>" | ||
| ] | ||
| }, | ||
| profilesCreate: { | ||
| description: "Create a taste profile", | ||
| usage: 'stow profiles create --name "My Profile" [options]', | ||
| examples: ['stow profiles create --name "My Profile" -b photos'] | ||
| }, | ||
| profilesGet: { | ||
| description: "Get a taste profile with clusters", | ||
| usage: "stow profiles get <id>", | ||
| examples: ["stow profiles get profile_abc123 --json"] | ||
| }, | ||
| profilesDelete: { | ||
| description: "Delete a taste profile", | ||
| usage: "stow profiles delete <id>", | ||
| examples: ["stow profiles delete profile_abc123"] | ||
| }, | ||
| jobs: { | ||
| description: "List processing jobs for a bucket", | ||
| usage: "stow jobs --bucket <id> [options]", | ||
| examples: [ | ||
| "stow jobs --bucket <id>", | ||
| "stow jobs --bucket <id> --status failed", | ||
| "stow jobs --bucket <id> --queue extract-colors --json" | ||
| ] | ||
| }, | ||
| jobsRetry: { | ||
| description: "Retry a failed job", | ||
| usage: "stow jobs retry <id> --queue <name> --bucket <id>", | ||
| examples: ["stow jobs retry job123 --queue generate-title --bucket <id>"] | ||
| }, | ||
| jobsDelete: { | ||
| description: "Remove a job", | ||
| usage: "stow jobs delete <id> --queue <name> --bucket <id>", | ||
| examples: ["stow jobs delete job123 --queue extract-colors --bucket <id>"] | ||
| }, | ||
| admin: { | ||
| description: "Admin commands (requires STOW_ADMIN_SECRET)", | ||
| usage: "stow admin <subcommand>", | ||
| examples: [ | ||
| "stow admin health", | ||
| "stow admin backfill dimensions --bucket <id> --dry-run", | ||
| "stow admin cleanup-drops --dry-run" | ||
| ], | ||
| notes: ["Requires STOW_ADMIN_SECRET environment variable."] | ||
| }, | ||
| adminHealth: { | ||
| description: "Check system health and queue depths", | ||
| usage: "stow admin health", | ||
| examples: ["stow admin health", "stow admin health --json"] | ||
| }, | ||
| adminBackfill: { | ||
| description: "Backfill processing data for files", | ||
| usage: "stow admin backfill <type> [options]", | ||
| examples: [ | ||
| "stow admin backfill dimensions --bucket <id> --dry-run", | ||
| "stow admin backfill colors --bucket <id> --limit 200", | ||
| "stow admin backfill embeddings --bucket <id> --limit 100 --json" | ||
| ], | ||
| notes: ["Valid types: dimensions, colors, embeddings"] | ||
| }, | ||
| adminCleanupDrops: { | ||
| description: "Remove expired drops", | ||
| usage: "stow admin cleanup-drops [options]", | ||
| examples: ["stow admin cleanup-drops --max-age-hours 24 --dry-run"] | ||
| }, | ||
| adminPurgeEvents: { | ||
| description: "Purge old webhook events", | ||
| usage: "stow admin purge-events [options]", | ||
| examples: ["stow admin purge-events --dry-run"] | ||
| }, | ||
| adminReconcileFiles: { | ||
| description: "Reconcile files between R2 and database", | ||
| usage: "stow admin reconcile-files --bucket <id>", | ||
| examples: ["stow admin reconcile-files --bucket <id> --dry-run"] | ||
| }, | ||
| adminRetrySyncFailures: { | ||
| description: "Retry failed S3 sync operations", | ||
| usage: "stow admin retry-sync-failures", | ||
| examples: ["stow admin retry-sync-failures"] | ||
| }, | ||
| adminJobs: { | ||
| description: "List and manage processing jobs", | ||
| usage: "stow admin jobs [options]", | ||
| examples: [ | ||
| "stow admin jobs", | ||
| "stow admin jobs --status failed", | ||
| "stow admin jobs --org <id> --queue generate-title", | ||
| "stow admin jobs --json" | ||
| ] | ||
| }, | ||
| adminJobsRetry: { | ||
| description: "Retry a failed job", | ||
| usage: "stow admin jobs retry <id> --queue <name>", | ||
| examples: ["stow admin jobs retry job123 --queue generate-title"] | ||
| }, | ||
| adminJobsDelete: { | ||
| description: "Remove a job", | ||
| usage: "stow admin jobs delete <id> --queue <name>", | ||
| examples: ["stow admin jobs delete job123 --queue extract-colors"] | ||
| }, | ||
| adminQueues: { | ||
| description: "Show queue depths and counts", | ||
| usage: "stow admin queues", | ||
| examples: ["stow admin queues", "stow admin queues --json"] | ||
| }, | ||
| adminQueuesClean: { | ||
| description: "Clean jobs from a queue", | ||
| usage: "stow admin queues clean <name> --failed|--completed", | ||
| examples: [ | ||
| "stow admin queues clean generate-title --failed", | ||
| "stow admin queues clean extract-colors --completed --grace 3600" | ||
| ] | ||
| } | ||
| }; | ||
| function renderCommandHelp(key) { | ||
| const doc = CLI_DOCS[key]; | ||
| const lines = [ | ||
| "", | ||
| `Usage: ${doc.usage}`, | ||
| "", | ||
| "Examples:", | ||
| ...doc.examples.map((example) => ` ${example}`) | ||
| ]; | ||
| if (doc.notes?.length) { | ||
| lines.push("", "Notes:", ...doc.notes.map((note) => ` ${note}`)); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| export { | ||
| CLI_DOCS, | ||
| renderCommandHelp | ||
| }; |
| // src/lib/validate-input.ts | ||
| import path from "path"; | ||
| var InputValidationError = class extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = "InputValidationError"; | ||
| } | ||
| }; | ||
| function hasControlCharacters(value) { | ||
| for (const char of value) { | ||
| const codePoint = char.codePointAt(0); | ||
| if (codePoint === void 0) { | ||
| continue; | ||
| } | ||
| if (codePoint >= 0 && codePoint <= 8 || codePoint === 11 || codePoint === 12 || codePoint >= 14 && codePoint <= 31) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| function validateInput(value, context) { | ||
| if (value.includes("../") || value.includes("..\\")) { | ||
| throw new InputValidationError(`${context}: path traversal not allowed`); | ||
| } | ||
| if (context !== "url" && value.includes("?")) { | ||
| throw new InputValidationError(`${context}: embedded query parameters not allowed`); | ||
| } | ||
| if (hasControlCharacters(value)) { | ||
| throw new InputValidationError(`${context}: control characters not allowed`); | ||
| } | ||
| if (/%25/.test(value)) { | ||
| throw new InputValidationError(`${context}: double-encoded values not allowed`); | ||
| } | ||
| if (/%2[fF]/.test(value) || /%2[eE]/.test(value)) { | ||
| throw new InputValidationError(`${context}: percent-encoded path characters not allowed`); | ||
| } | ||
| if (context !== "url" && value.includes("#")) { | ||
| throw new InputValidationError(`${context}: embedded hash fragments not allowed`); | ||
| } | ||
| return value; | ||
| } | ||
| function validateBucketName(name) { | ||
| return validateInput(name, "bucket name"); | ||
| } | ||
| function validateFileKey(key) { | ||
| return validateInput(key, "file key"); | ||
| } | ||
| export { | ||
| InputValidationError, | ||
| validateInput, | ||
| validateBucketName, | ||
| validateFileKey | ||
| }; |
| // src/lib/format.ts | ||
| function padCell(str, width) { | ||
| return str.padEnd(width); | ||
| } | ||
| function formatBytes(bytes) { | ||
| if (bytes === 0) { | ||
| return "0 B"; | ||
| } | ||
| const units = ["B", "KB", "MB", "GB", "TB"]; | ||
| const i = Math.floor(Math.log(bytes) / Math.log(1024)); | ||
| return `${(bytes / 1024 ** i).toFixed(i > 0 ? 1 : 0)} ${units[i]}`; | ||
| } | ||
| function formatTable(headers, rows) { | ||
| if (rows.length === 0) { | ||
| return ""; | ||
| } | ||
| const widths = headers.map((h, i) => { | ||
| let dataMax = 0; | ||
| for (const row of rows) { | ||
| const cellLength = (row[i] || "").length; | ||
| if (cellLength > dataMax) { | ||
| dataMax = cellLength; | ||
| } | ||
| } | ||
| return Math.max(h.length, dataMax); | ||
| }); | ||
| const sep = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u2500"); | ||
| const lines = [headers.map((h, i) => padCell(h, widths[i] ?? 0)).join(" "), sep]; | ||
| for (const row of rows) { | ||
| lines.push(row.map((cell, i) => padCell(cell || "", widths[i] ?? 0)).join(" ")); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| function usageBar(used, total, width = 20) { | ||
| const ratio = Math.min(used / total, 1); | ||
| const filled = Math.round(ratio * width); | ||
| const empty = width - filled; | ||
| const pct = Math.round(ratio * 100); | ||
| return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}] ${pct}%`; | ||
| } | ||
| export { | ||
| formatBytes, | ||
| formatTable, | ||
| usageBar | ||
| }; |
| // src/lib/sanitize-response.ts | ||
| var INJECTION_PATTERNS = [ | ||
| // Direct instruction injection | ||
| /\b(?:ignore|disregard|forget)\b.*\b(?:previous|above|prior)\b.*\b(?:instructions?|rules?|context)\b/i, | ||
| // System prompt extraction attempts | ||
| /\b(?:reveal|show|print|output|display)\b.*\b(?:system\s*prompt|instructions?|rules?)\b/i, | ||
| // Role hijacking | ||
| /\byou\s+are\s+(?:now|a)\b/i, | ||
| // Tool/action injection | ||
| /\b(?:execute|run|call)\b.*\b(?:command|tool|function|bash|shell)\b/i, | ||
| // Markdown/XML injection that could affect agent parsing | ||
| /<\/?(?:system|user|assistant|tool_use|tool_result)\b/i | ||
| ]; | ||
| var USER_CONTENT_FIELDS = /* @__PURE__ */ new Set([ | ||
| "originalFilename", | ||
| "filename", | ||
| "name", | ||
| "description", | ||
| "label", | ||
| "text", | ||
| "slug", | ||
| "webhookUrl" | ||
| ]); | ||
| function detectInjection(value) { | ||
| return INJECTION_PATTERNS.some((pattern) => pattern.test(value)); | ||
| } | ||
| function sanitizeValue(value) { | ||
| if (detectInjection(value)) { | ||
| return `[FLAGGED: potential prompt injection] ${value}`; | ||
| } | ||
| return value; | ||
| } | ||
| function sanitizeResponse(data) { | ||
| if (data === null || data === void 0) { | ||
| return data; | ||
| } | ||
| if (typeof data === "string") { | ||
| return sanitizeValue(data); | ||
| } | ||
| if (Array.isArray(data)) { | ||
| return data.map((item) => sanitizeResponse(item)); | ||
| } | ||
| if (typeof data === "object") { | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(data)) { | ||
| if (USER_CONTENT_FIELDS.has(key) && typeof value === "string") { | ||
| result[key] = sanitizeValue(value); | ||
| } else if (typeof value === "object" && value !== null) { | ||
| result[key] = sanitizeResponse(value); | ||
| } else { | ||
| result[key] = value; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| return data; | ||
| } | ||
| // src/lib/output.ts | ||
| var _forceHuman = false; | ||
| var _globalFields; | ||
| var _globalNdjson = false; | ||
| function setForceHuman(value) { | ||
| _forceHuman = value; | ||
| } | ||
| function setGlobalFields(fields) { | ||
| _globalFields = fields; | ||
| } | ||
| function setGlobalNdjson(value) { | ||
| _globalNdjson = value; | ||
| } | ||
| function isJsonOutput() { | ||
| if (_forceHuman) { | ||
| return false; | ||
| } | ||
| return !process.stdout.isTTY; | ||
| } | ||
| function output(data, humanFormatter, options) { | ||
| const sanitized = sanitizeResponse(data); | ||
| const unwrapped = _globalFields || _globalNdjson ? unwrapArray(sanitized) : sanitized; | ||
| const masked = applyFieldMask(unwrapped, _globalFields); | ||
| if (_globalNdjson && Array.isArray(masked)) { | ||
| outputNdjson(masked); | ||
| return; | ||
| } | ||
| if (options?.json || isJsonOutput()) { | ||
| console.log(JSON.stringify(masked, null, 2)); | ||
| } else if (humanFormatter && !_globalFields) { | ||
| console.log(humanFormatter()); | ||
| } else { | ||
| console.log(JSON.stringify(masked, null, 2)); | ||
| } | ||
| } | ||
| function outputError(error, code, details) { | ||
| if (isJsonOutput()) { | ||
| console.error( | ||
| JSON.stringify({ error, ...code ? { code } : {}, ...details }) | ||
| ); | ||
| } else { | ||
| console.error(`Error: ${error}`); | ||
| } | ||
| process.exit(1); | ||
| } | ||
| function outputNdjson(items) { | ||
| for (const item of items) { | ||
| const sanitized = sanitizeResponse(item); | ||
| console.log(JSON.stringify(sanitized)); | ||
| } | ||
| } | ||
| function applyFieldMask(data, fields) { | ||
| if (!fields) { | ||
| return data; | ||
| } | ||
| const fieldSet = new Set(fields.split(",").map((f) => f.trim())); | ||
| if (Array.isArray(data)) { | ||
| return data.map((item) => pickFields(item, fieldSet)); | ||
| } | ||
| if (typeof data === "object" && data !== null) { | ||
| return pickFields(data, fieldSet); | ||
| } | ||
| return data; | ||
| } | ||
| function unwrapArray(data) { | ||
| if (typeof data !== "object" || data === null || Array.isArray(data)) { | ||
| return data; | ||
| } | ||
| const entries = Object.entries(data); | ||
| if (entries.length === 1 && Array.isArray(entries[0][1])) { | ||
| return entries[0][1]; | ||
| } | ||
| return data; | ||
| } | ||
| function pickFields(obj, fieldSet) { | ||
| if (typeof obj !== "object" || obj === null) { | ||
| return {}; | ||
| } | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| if (fieldSet.has(key)) { | ||
| result[key] = value; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| export { | ||
| setForceHuman, | ||
| setGlobalFields, | ||
| setGlobalNdjson, | ||
| isJsonOutput, | ||
| output, | ||
| outputError | ||
| }; |
| // src/lib/parse-json-input.ts | ||
| function stripUndefined(obj) { | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| if (value !== void 0) { | ||
| result[key] = value; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| function parseJsonInput(jsonStr, flagValues) { | ||
| if (!jsonStr) { | ||
| return flagValues; | ||
| } | ||
| let parsed; | ||
| try { | ||
| parsed = JSON.parse(jsonStr); | ||
| } catch { | ||
| throw new Error(`Invalid JSON in --input-json: ${jsonStr.slice(0, 100)}`); | ||
| } | ||
| if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { | ||
| throw new Error("--input-json must be a JSON object"); | ||
| } | ||
| return { ...parsed, ...stripUndefined(flagValues) }; | ||
| } | ||
| export { | ||
| parseJsonInput | ||
| }; |
| import { | ||
| validateBucketName, | ||
| validateFileKey | ||
| } from "./chunk-533UGNLM.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/delete.ts | ||
| async function deleteFile(bucket, key, options = {}) { | ||
| validateBucketName(bucket); | ||
| validateFileKey(key); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "deleteFile", | ||
| details: { bucket, key } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| await stow.deleteFile(key, { bucket }); | ||
| console.log(`Deleted: ${key} from ${bucket}`); | ||
| } | ||
| export { | ||
| deleteFile | ||
| }; |
| import { | ||
| validateBucketName, | ||
| validateFileKey | ||
| } from "./chunk-NBHBVKP5.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/delete.ts | ||
| async function deleteFile(bucket, key, options = {}) { | ||
| validateBucketName(bucket); | ||
| validateFileKey(key); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "deleteFile", | ||
| details: { bucket, key } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| await stow.deleteFile(key, { bucket }); | ||
| console.log(`Deleted: ${key} from ${bucket}`); | ||
| } | ||
| export { | ||
| deleteFile | ||
| }; |
| import { | ||
| CLI_DOCS | ||
| } from "./chunk-MYFLRBWC.js"; | ||
| import { | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| // src/commands/describe.ts | ||
| var COMMAND_MAP = { | ||
| upload: "upload", | ||
| drop: "drop", | ||
| delete: "delete", | ||
| whoami: "whoami", | ||
| open: "open", | ||
| buckets: "buckets", | ||
| "buckets.create": "bucketsCreate", | ||
| "buckets.rename": "bucketsRename", | ||
| "buckets.delete": "bucketsDelete", | ||
| files: "files", | ||
| "files.get": "filesGet", | ||
| "files.update": "filesUpdate", | ||
| "files.enrich": "filesEnrich", | ||
| "files.missing": "filesMissing", | ||
| search: "search", | ||
| "search.text": "searchText", | ||
| "search.similar": "searchSimilar", | ||
| "search.color": "searchColor", | ||
| "search.diverse": "searchDiverse", | ||
| tags: "tags", | ||
| "tags.create": "tagsCreate", | ||
| "tags.delete": "tagsDelete", | ||
| drops: "drops", | ||
| "drops.delete": "dropsDelete", | ||
| profiles: "profiles", | ||
| "profiles.create": "profilesCreate", | ||
| "profiles.get": "profilesGet", | ||
| "profiles.delete": "profilesDelete", | ||
| jobs: "jobs", | ||
| "jobs.retry": "jobsRetry", | ||
| "jobs.delete": "jobsDelete", | ||
| admin: "admin", | ||
| "admin.health": "adminHealth", | ||
| "admin.backfill": "adminBackfill", | ||
| "admin.cleanup-drops": "adminCleanupDrops", | ||
| "admin.purge-events": "adminPurgeEvents", | ||
| "admin.reconcile-files": "adminReconcileFiles", | ||
| "admin.retry-sync-failures": "adminRetrySyncFailures", | ||
| "admin.jobs": "adminJobs", | ||
| "admin.jobs.retry": "adminJobsRetry", | ||
| "admin.jobs.delete": "adminJobsDelete", | ||
| "admin.queues": "adminQueues", | ||
| "admin.queues.clean": "adminQueuesClean" | ||
| }; | ||
| function describeCommand(commandPath) { | ||
| if (!commandPath) { | ||
| output({ | ||
| commands: Object.keys(COMMAND_MAP).sort() | ||
| }); | ||
| return; | ||
| } | ||
| const docKey = COMMAND_MAP[commandPath]; | ||
| if (!(docKey && CLI_DOCS[docKey])) { | ||
| console.error(`Unknown command: ${commandPath}`); | ||
| console.error(`Available: ${Object.keys(COMMAND_MAP).sort().join(", ")}`); | ||
| process.exit(1); | ||
| return; | ||
| } | ||
| const doc = CLI_DOCS[docKey]; | ||
| output({ | ||
| command: commandPath, | ||
| description: doc.description, | ||
| usage: doc.usage, | ||
| examples: doc.examples, | ||
| notes: doc.notes ?? [] | ||
| }); | ||
| } | ||
| export { | ||
| describeCommand | ||
| }; |
| import { | ||
| CLI_DOCS | ||
| } from "./chunk-XJDK2CBE.js"; | ||
| import { | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| // src/commands/describe.ts | ||
| var COMMAND_MAP = { | ||
| upload: "upload", | ||
| drop: "drop", | ||
| delete: "delete", | ||
| whoami: "whoami", | ||
| open: "open", | ||
| buckets: "buckets", | ||
| "buckets.create": "bucketsCreate", | ||
| "buckets.rename": "bucketsRename", | ||
| "buckets.delete": "bucketsDelete", | ||
| files: "files", | ||
| "files.get": "filesGet", | ||
| "files.update": "filesUpdate", | ||
| "files.enrich": "filesEnrich", | ||
| "files.missing": "filesMissing", | ||
| search: "search", | ||
| "search.text": "searchText", | ||
| "search.similar": "searchSimilar", | ||
| "search.color": "searchColor", | ||
| "search.diverse": "searchDiverse", | ||
| tags: "tags", | ||
| "tags.create": "tagsCreate", | ||
| "tags.delete": "tagsDelete", | ||
| drops: "drops", | ||
| "drops.delete": "dropsDelete", | ||
| profiles: "profiles", | ||
| "profiles.create": "profilesCreate", | ||
| "profiles.get": "profilesGet", | ||
| "profiles.delete": "profilesDelete", | ||
| jobs: "jobs", | ||
| "jobs.retry": "jobsRetry", | ||
| "jobs.delete": "jobsDelete", | ||
| admin: "admin", | ||
| "admin.health": "adminHealth", | ||
| "admin.backfill": "adminBackfill", | ||
| "admin.cleanup-drops": "adminCleanupDrops", | ||
| "admin.purge-events": "adminPurgeEvents", | ||
| "admin.reconcile-files": "adminReconcileFiles", | ||
| "admin.retry-sync-failures": "adminRetrySyncFailures", | ||
| "admin.jobs": "adminJobs", | ||
| "admin.jobs.retry": "adminJobsRetry", | ||
| "admin.jobs.delete": "adminJobsDelete", | ||
| "admin.queues": "adminQueues", | ||
| "admin.queues.clean": "adminQueuesClean" | ||
| }; | ||
| function describeCommand(commandPath) { | ||
| if (!commandPath) { | ||
| output({ | ||
| commands: Object.keys(COMMAND_MAP).sort() | ||
| }); | ||
| return; | ||
| } | ||
| const docKey = COMMAND_MAP[commandPath]; | ||
| if (!(docKey && CLI_DOCS[docKey])) { | ||
| console.error(`Unknown command: ${commandPath}`); | ||
| console.error(`Available: ${Object.keys(COMMAND_MAP).sort().join(", ")}`); | ||
| process.exit(1); | ||
| return; | ||
| } | ||
| const doc = CLI_DOCS[docKey]; | ||
| output({ | ||
| command: commandPath, | ||
| description: doc.description, | ||
| usage: doc.usage, | ||
| examples: doc.examples, | ||
| notes: doc.notes ?? [] | ||
| }); | ||
| } | ||
| export { | ||
| describeCommand | ||
| }; |
| import { | ||
| formatBytes, | ||
| formatTable, | ||
| usageBar | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/drops.ts | ||
| async function listDrops() { | ||
| const stow = createStow(); | ||
| const data = await stow.listDrops(); | ||
| if (data.drops.length === 0) { | ||
| console.log("No drops yet. Create one with: stow drop <file>"); | ||
| return; | ||
| } | ||
| const rows = data.drops.map((d) => [ | ||
| d.filename, | ||
| formatBytes(Number(d.size)), | ||
| d.contentType, | ||
| d.url | ||
| ]); | ||
| console.log(formatTable(["Filename", "Size", "Type", "URL"], rows)); | ||
| console.log( | ||
| ` | ||
| Storage: ${usageBar(data.usage.bytes, data.usage.limit)} ${formatBytes(data.usage.bytes)} / ${formatBytes(data.usage.limit)}` | ||
| ); | ||
| } | ||
| async function deleteDrop(id) { | ||
| const stow = createStow(); | ||
| await stow.deleteDrop(id); | ||
| console.log(`Deleted drop: ${id}`); | ||
| } | ||
| export { | ||
| deleteDrop, | ||
| listDrops | ||
| }; |
| import { | ||
| parseJsonInput | ||
| } from "./chunk-AHBVZRDR.js"; | ||
| import { | ||
| validateBucketName, | ||
| validateFileKey | ||
| } from "./chunk-533UGNLM.js"; | ||
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import { | ||
| formatBytes, | ||
| formatTable | ||
| } from "./chunk-FZGOTXTE.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import { | ||
| getApiKey, | ||
| getBaseUrl | ||
| } from "./chunk-TOADDO2F.js"; | ||
| // src/commands/files.ts | ||
| async function listFiles(bucket, options) { | ||
| const stow = createStow(); | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const data = await stow.listFiles({ | ||
| bucket, | ||
| ...options.search ? { prefix: options.search } : {}, | ||
| ...parsedLimit && Number.isFinite(parsedLimit) && parsedLimit > 0 ? { limit: parsedLimit } : {} | ||
| }); | ||
| if (data.files.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log(`No files in bucket '${bucket}'.`); | ||
| } | ||
| return; | ||
| } | ||
| if (options.json || isJsonOutput()) { | ||
| output(data); | ||
| return; | ||
| } | ||
| const rows = data.files.map((f) => [ | ||
| f.key, | ||
| formatBytes(f.size), | ||
| f.lastModified.split("T")[0] ?? f.lastModified | ||
| ]); | ||
| console.log(formatTable(["Key", "Size", "Modified"], rows)); | ||
| if (data.nextCursor) { | ||
| console.log("\n(more files available \u2014 use --limit to see more)"); | ||
| } | ||
| } | ||
| async function getFile(bucket, key, _options) { | ||
| const stow = createStow(); | ||
| const file = await stow.getFile(key, { bucket }); | ||
| output(file, () => { | ||
| const lines = [ | ||
| formatTable( | ||
| ["Field", "Value"], | ||
| [ | ||
| ["Key", file.key], | ||
| ["Size", formatBytes(file.size)], | ||
| ["Type", file.contentType], | ||
| ["Created", file.createdAt], | ||
| ["URL", file.url ?? "(private)"], | ||
| [ | ||
| "Dimensions", | ||
| file.width && file.height ? `${file.width}\xD7${file.height}` : "\u2014" | ||
| ], | ||
| ["Duration", file.duration ? `${file.duration}s` : "\u2014"], | ||
| ["Embedding", file.embeddingStatus ?? "\u2014"] | ||
| ] | ||
| ) | ||
| ]; | ||
| if (file.metadata && Object.keys(file.metadata).length > 0) { | ||
| lines.push("\nMetadata:"); | ||
| for (const [k, v] of Object.entries(file.metadata)) { | ||
| lines.push(` ${k}: ${v}`); | ||
| } | ||
| } | ||
| return lines.join("\n"); | ||
| }); | ||
| } | ||
| async function updateFile(bucket, key, options) { | ||
| validateBucketName(bucket); | ||
| validateFileKey(key); | ||
| const flagMetadata = {}; | ||
| if (options.metadata && options.metadata.length > 0) { | ||
| for (const pair of options.metadata) { | ||
| const idx = pair.indexOf("="); | ||
| if (idx === -1) { | ||
| console.error( | ||
| `Error: Invalid metadata format '${pair}'. Use key=value.` | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| flagMetadata[pair.slice(0, idx)] = pair.slice(idx + 1); | ||
| } | ||
| } | ||
| const jsonInput = parseJsonInput( | ||
| options.inputJson, | ||
| {} | ||
| ); | ||
| const hasFlagMetadata = Object.keys(flagMetadata).length > 0; | ||
| const metadata = hasFlagMetadata ? { ...jsonInput.metadata ?? {}, ...flagMetadata } : jsonInput.metadata; | ||
| if (!metadata || Object.keys(metadata).length === 0) { | ||
| console.error( | ||
| "Error: At least one -m key=value pair or --input-json with metadata is required." | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "updateFile", | ||
| details: { bucket, key, metadata } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| const file = await stow.updateFileMetadata(key, metadata, { bucket }); | ||
| output(file, () => `Updated ${key}`); | ||
| } | ||
| async function enrichFile(bucket, key) { | ||
| const stow = createStow(); | ||
| const results = await Promise.allSettled([ | ||
| stow.generateTitle(key, { bucket }), | ||
| stow.generateDescription(key, { bucket }), | ||
| stow.generateAltText(key, { bucket }) | ||
| ]); | ||
| const labels = ["Title", "Description", "Alt text"]; | ||
| for (let i = 0; i < results.length; i++) { | ||
| const result = results[i]; | ||
| const label = labels[i]; | ||
| if (result.status === "fulfilled") { | ||
| console.log(` ${label}: triggered`); | ||
| } else { | ||
| console.error(` ${label}: failed \u2014 ${result.reason}`); | ||
| } | ||
| } | ||
| const succeeded = results.filter((r) => r.status === "fulfilled").length; | ||
| console.log( | ||
| ` | ||
| Enriched ${key}: ${succeeded}/${results.length} tasks dispatched` | ||
| ); | ||
| } | ||
| async function listMissing(bucket, type, options) { | ||
| const validTypes = ["dimensions", "embeddings", "colors"]; | ||
| if (!validTypes.includes(type)) { | ||
| console.error( | ||
| `Error: Invalid type '${type}'. Must be one of: ${validTypes.join(", ")}` | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const baseUrl = getBaseUrl(); | ||
| const apiKey = getApiKey(); | ||
| const params = new URLSearchParams({ | ||
| bucket, | ||
| missing: type, | ||
| ...parsedLimit && Number.isFinite(parsedLimit) && parsedLimit > 0 ? { limit: String(parsedLimit) } : {} | ||
| }); | ||
| const res = await fetch(`${baseUrl}/files?${params}`, { | ||
| headers: { "x-api-key": apiKey } | ||
| }); | ||
| if (!res.ok) { | ||
| const body = await res.json().catch(() => ({})); | ||
| throw new Error(body.error ?? `HTTP ${res.status}`); | ||
| } | ||
| const data = await res.json(); | ||
| if (data.files.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log(`No files missing ${type} in bucket '${bucket}'.`); | ||
| } | ||
| return; | ||
| } | ||
| if (options.json || isJsonOutput()) { | ||
| output(data); | ||
| return; | ||
| } | ||
| const rows = data.files.map((f) => [ | ||
| f.key, | ||
| formatBytes(f.size), | ||
| f.lastModified.split("T")[0] ?? f.lastModified | ||
| ]); | ||
| console.log(formatTable(["Key", "Size", "Modified"], rows)); | ||
| console.log(` | ||
| ${data.files.length} files missing ${type}`); | ||
| } | ||
| export { | ||
| enrichFile, | ||
| getFile, | ||
| listFiles, | ||
| listMissing, | ||
| updateFile | ||
| }; |
| import { | ||
| parseJsonInput | ||
| } from "./chunk-XVKIRHTX.js"; | ||
| import { | ||
| validateBucketName, | ||
| validateFileKey | ||
| } from "./chunk-NBHBVKP5.js"; | ||
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import { | ||
| formatBytes, | ||
| formatTable | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import { | ||
| getApiKey, | ||
| getBaseUrl | ||
| } from "./chunk-TOADDO2F.js"; | ||
| // src/commands/files.ts | ||
| async function listFiles(bucket, options) { | ||
| const stow = createStow(); | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const data = await stow.listFiles({ | ||
| bucket, | ||
| ...options.search ? { prefix: options.search } : {}, | ||
| ...parsedLimit && Number.isFinite(parsedLimit) && parsedLimit > 0 ? { limit: parsedLimit } : {} | ||
| }); | ||
| if (data.files.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log(`No files in bucket '${bucket}'.`); | ||
| } | ||
| return; | ||
| } | ||
| if (options.json || isJsonOutput()) { | ||
| output(data); | ||
| return; | ||
| } | ||
| const rows = data.files.map((f) => [ | ||
| f.key, | ||
| formatBytes(f.size), | ||
| f.lastModified.split("T")[0] ?? f.lastModified | ||
| ]); | ||
| console.log(formatTable(["Key", "Size", "Modified"], rows)); | ||
| if (data.nextCursor) { | ||
| console.log("\n(more files available \u2014 use --limit to see more)"); | ||
| } | ||
| } | ||
| async function getFile(bucket, key, _options) { | ||
| const stow = createStow(); | ||
| const file = await stow.getFile(key, { bucket }); | ||
| output(file, () => { | ||
| const lines = [ | ||
| formatTable( | ||
| ["Field", "Value"], | ||
| [ | ||
| ["Key", file.key], | ||
| ["Size", formatBytes(file.size)], | ||
| ["Type", file.contentType], | ||
| ["Created", file.createdAt], | ||
| ["URL", file.url ?? "(private)"], | ||
| ["Dimensions", file.width && file.height ? `${file.width}\xD7${file.height}` : "\u2014"], | ||
| ["Duration", file.duration ? `${file.duration}s` : "\u2014"], | ||
| ["Embedding", file.embeddingStatus ?? "\u2014"] | ||
| ] | ||
| ) | ||
| ]; | ||
| if (file.metadata && Object.keys(file.metadata).length > 0) { | ||
| lines.push("\nMetadata:"); | ||
| for (const [k, v] of Object.entries(file.metadata)) { | ||
| lines.push(` ${k}: ${v}`); | ||
| } | ||
| } | ||
| return lines.join("\n"); | ||
| }); | ||
| } | ||
| async function updateFile(bucket, key, options) { | ||
| validateBucketName(bucket); | ||
| validateFileKey(key); | ||
| const flagMetadata = {}; | ||
| if (options.metadata && options.metadata.length > 0) { | ||
| for (const pair of options.metadata) { | ||
| const idx = pair.indexOf("="); | ||
| if (idx === -1) { | ||
| console.error(`Error: Invalid metadata format '${pair}'. Use key=value.`); | ||
| process.exit(1); | ||
| } | ||
| flagMetadata[pair.slice(0, idx)] = pair.slice(idx + 1); | ||
| } | ||
| } | ||
| const jsonInput = parseJsonInput(options.inputJson, {}); | ||
| const hasFlagMetadata = Object.keys(flagMetadata).length > 0; | ||
| const metadata = hasFlagMetadata ? { ...jsonInput.metadata, ...flagMetadata } : jsonInput.metadata; | ||
| if (!metadata || Object.keys(metadata).length === 0) { | ||
| console.error( | ||
| "Error: At least one -m key=value pair or --input-json with metadata is required." | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "updateFile", | ||
| details: { bucket, key, metadata } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| const file = await stow.updateFileMetadata(key, metadata, { bucket }); | ||
| output(file, () => `Updated ${key}`); | ||
| } | ||
| async function enrichFile(bucket, key) { | ||
| const stow = createStow(); | ||
| const results = await Promise.allSettled([ | ||
| stow.generateTitle(key, { bucket }), | ||
| stow.generateDescription(key, { bucket }), | ||
| stow.generateAltText(key, { bucket }) | ||
| ]); | ||
| const labels = ["Title", "Description", "Alt text"]; | ||
| for (let i = 0; i < results.length; i += 1) { | ||
| const result = results[i]; | ||
| const label = labels[i]; | ||
| if (result.status === "fulfilled") { | ||
| console.log(` ${label}: triggered`); | ||
| } else { | ||
| console.error(` ${label}: failed \u2014 ${result.reason}`); | ||
| } | ||
| } | ||
| const succeeded = results.filter((r) => r.status === "fulfilled").length; | ||
| console.log(` | ||
| Enriched ${key}: ${succeeded}/${results.length} tasks dispatched`); | ||
| } | ||
| async function listMissing(bucket, type, options) { | ||
| const validTypes = ["dimensions", "embeddings", "colors"]; | ||
| if (!validTypes.includes(type)) { | ||
| console.error(`Error: Invalid type '${type}'. Must be one of: ${validTypes.join(", ")}`); | ||
| process.exit(1); | ||
| } | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const baseUrl = getBaseUrl(); | ||
| const apiKey = getApiKey(); | ||
| const params = new URLSearchParams({ | ||
| bucket, | ||
| missing: type, | ||
| ...parsedLimit && Number.isFinite(parsedLimit) && parsedLimit > 0 ? { limit: String(parsedLimit) } : {} | ||
| }); | ||
| const res = await fetch(`${baseUrl}/files?${params}`, { | ||
| headers: { "x-api-key": apiKey } | ||
| }); | ||
| if (!res.ok) { | ||
| const body = await res.json().catch(() => ({})); | ||
| throw new Error(body.error ?? `HTTP ${res.status}`); | ||
| } | ||
| const data = await res.json(); | ||
| if (data.files.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log(`No files missing ${type} in bucket '${bucket}'.`); | ||
| } | ||
| return; | ||
| } | ||
| if (options.json || isJsonOutput()) { | ||
| output(data); | ||
| return; | ||
| } | ||
| const rows = data.files.map((f) => [ | ||
| f.key, | ||
| formatBytes(f.size), | ||
| f.lastModified.split("T")[0] ?? f.lastModified | ||
| ]); | ||
| console.log(formatTable(["Key", "Size", "Modified"], rows)); | ||
| console.log(` | ||
| ${data.files.length} files missing ${type}`); | ||
| } | ||
| export { | ||
| enrichFile, | ||
| getFile, | ||
| listFiles, | ||
| listMissing, | ||
| updateFile | ||
| }; |
| import { | ||
| adminRequest | ||
| } from "./chunk-QF7PVPWQ.js"; | ||
| import { | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/admin/health.ts | ||
| async function health(options) { | ||
| const result = await adminRequest({ | ||
| method: "GET", | ||
| path: "/health" | ||
| }); | ||
| output( | ||
| result, | ||
| () => { | ||
| const lines = []; | ||
| const statusIcon = result.status === "ok" ? "+" : "x"; | ||
| lines.push(`${statusIcon} ${result.status} (${result.version})`); | ||
| lines.push(` ${result.timestamp}`); | ||
| lines.push("\nChecks:"); | ||
| for (const [name, status] of Object.entries(result.checks)) { | ||
| const icon = status === "ok" ? "+" : "x"; | ||
| lines.push(` ${icon} ${name}`); | ||
| } | ||
| if (result.queues) { | ||
| lines.push("\nQueues:"); | ||
| const rows = []; | ||
| for (const [name, counts] of Object.entries(result.queues)) { | ||
| if (counts === "unavailable") { | ||
| rows.push([name, "--", "--", "--", "--"]); | ||
| } else { | ||
| const c = counts; | ||
| rows.push([ | ||
| name, | ||
| String(c.waiting ?? 0), | ||
| String(c.active ?? 0), | ||
| String(c.completed ?? 0), | ||
| String(c.failed ?? 0) | ||
| ]); | ||
| } | ||
| } | ||
| lines.push(formatTable(["Queue", "Waiting", "Active", "Completed", "Failed"], rows)); | ||
| } | ||
| return lines.join("\n"); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| export { | ||
| health | ||
| }; |
| import { | ||
| adminRequest | ||
| } from "./chunk-QF7PVPWQ.js"; | ||
| import { | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-FZGOTXTE.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/admin/health.ts | ||
| async function health(options) { | ||
| const result = await adminRequest({ | ||
| method: "GET", | ||
| path: "/health" | ||
| }); | ||
| output( | ||
| result, | ||
| () => { | ||
| const lines = []; | ||
| const statusIcon = result.status === "ok" ? "+" : "x"; | ||
| lines.push(`${statusIcon} ${result.status} (${result.version})`); | ||
| lines.push(` ${result.timestamp}`); | ||
| lines.push("\nChecks:"); | ||
| for (const [name, status] of Object.entries(result.checks)) { | ||
| const icon = status === "ok" ? "+" : "x"; | ||
| lines.push(` ${icon} ${name}`); | ||
| } | ||
| if (result.queues) { | ||
| lines.push("\nQueues:"); | ||
| const rows = []; | ||
| for (const [name, counts] of Object.entries(result.queues)) { | ||
| if (counts === "unavailable") { | ||
| rows.push([name, "--", "--", "--", "--"]); | ||
| } else { | ||
| const c = counts; | ||
| rows.push([ | ||
| name, | ||
| String(c.waiting ?? 0), | ||
| String(c.active ?? 0), | ||
| String(c.completed ?? 0), | ||
| String(c.failed ?? 0) | ||
| ]); | ||
| } | ||
| } | ||
| lines.push( | ||
| formatTable( | ||
| ["Queue", "Waiting", "Active", "Completed", "Failed"], | ||
| rows | ||
| ) | ||
| ); | ||
| } | ||
| return lines.join("\n"); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| export { | ||
| health | ||
| }; |
| import { | ||
| adminRequest | ||
| } from "./chunk-QF7PVPWQ.js"; | ||
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/admin/jobs.ts | ||
| function formatTimestamp(ts) { | ||
| return new Date(ts).toISOString().replace("T", " ").slice(0, 19); | ||
| } | ||
| async function listAdminJobs(options) { | ||
| const params = new URLSearchParams(); | ||
| if (options.org) { | ||
| params.set("orgId", options.org); | ||
| } | ||
| if (options.bucket) { | ||
| params.set("bucketId", options.bucket); | ||
| } | ||
| if (options.status) { | ||
| params.set("status", options.status); | ||
| } | ||
| if (options.queue) { | ||
| params.set("queue", options.queue); | ||
| } | ||
| if (options.limit) { | ||
| params.set("limit", options.limit); | ||
| } | ||
| const qs = params.toString(); | ||
| const result = await adminRequest({ | ||
| method: "GET", | ||
| path: `/admin/jobs${qs ? `?${qs}` : ""}` | ||
| }); | ||
| if (result.jobs.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(result.jobs, void 0, { json: options.json }); | ||
| } else { | ||
| console.log("No jobs found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| result.jobs, | ||
| () => { | ||
| const rows = result.jobs.map((job) => [ | ||
| job.jobId, | ||
| job.queueName, | ||
| job.status, | ||
| `${job.data.fileId.slice(0, 8)}...`, | ||
| `${job.data.orgId.slice(0, 8)}...`, | ||
| formatTimestamp(job.timestamp), | ||
| job.failedReason ? job.failedReason.slice(0, 40) : "" | ||
| ]); | ||
| return formatTable(["ID", "Queue", "Status", "File", "Org", "Created", "Error"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function retryAdminJob(jobId, options) { | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/admin/jobs/${jobId}/retry`, | ||
| body: { queue: options.queue } | ||
| }); | ||
| if (result.retried) { | ||
| console.log(`Job ${jobId} retried.`); | ||
| } | ||
| } | ||
| async function deleteAdminJob(jobId, options) { | ||
| const result = await adminRequest({ | ||
| method: "DELETE", | ||
| path: `/admin/jobs/${jobId}?queue=${encodeURIComponent(options.queue)}` | ||
| }); | ||
| if (result.deleted) { | ||
| console.log(`Job ${jobId} removed.`); | ||
| } | ||
| } | ||
| export { | ||
| deleteAdminJob, | ||
| listAdminJobs, | ||
| retryAdminJob | ||
| }; |
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import { | ||
| getApiKey, | ||
| getBaseUrl | ||
| } from "./chunk-TOADDO2F.js"; | ||
| // src/commands/jobs.ts | ||
| async function bucketRequest(opts) { | ||
| const baseUrl = getBaseUrl(); | ||
| const apiKey = getApiKey(); | ||
| const res = await fetch(`${baseUrl}${opts.path}`, { | ||
| method: opts.method, | ||
| headers: { | ||
| "x-api-key": apiKey, | ||
| ...opts.body ? { "Content-Type": "application/json" } : {} | ||
| }, | ||
| ...opts.body ? { body: JSON.stringify(opts.body) } : {} | ||
| }); | ||
| if (!res.ok) { | ||
| const body = await res.json().catch(() => ({ error: res.statusText })); | ||
| const message = body.error ?? `HTTP ${res.status}`; | ||
| throw new Error(message); | ||
| } | ||
| return await res.json(); | ||
| } | ||
| function formatTimestamp(ts) { | ||
| return new Date(ts).toISOString().replace("T", " ").slice(0, 19); | ||
| } | ||
| async function listJobs(bucketId, options) { | ||
| const params = new URLSearchParams(); | ||
| if (options.status) { | ||
| params.set("status", options.status); | ||
| } | ||
| if (options.queue) { | ||
| params.set("queue", options.queue); | ||
| } | ||
| if (options.limit) { | ||
| params.set("limit", options.limit); | ||
| } | ||
| const qs = params.toString(); | ||
| const path = `/buckets/${bucketId}/jobs${qs ? `?${qs}` : ""}`; | ||
| const result = await bucketRequest({ | ||
| method: "GET", | ||
| path | ||
| }); | ||
| if (result.jobs.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(result.jobs); | ||
| } else { | ||
| console.log("No jobs found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| result.jobs, | ||
| () => { | ||
| const rows = result.jobs.map((job) => [ | ||
| job.jobId, | ||
| job.queueName, | ||
| job.status, | ||
| `${job.data.fileId.slice(0, 8)}...`, | ||
| formatTimestamp(job.timestamp), | ||
| job.failedReason ? job.failedReason.slice(0, 40) : "" | ||
| ]); | ||
| return formatTable(["ID", "Queue", "Status", "File", "Created", "Error"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function retryJob(jobId, options) { | ||
| const result = await bucketRequest({ | ||
| method: "POST", | ||
| path: `/buckets/${options.bucket}/jobs/${jobId}/retry`, | ||
| body: { queue: options.queue } | ||
| }); | ||
| if (result.retried) { | ||
| console.log(`Job ${jobId} retried.`); | ||
| } | ||
| } | ||
| async function deleteJob(jobId, options) { | ||
| const result = await bucketRequest({ | ||
| method: "DELETE", | ||
| path: `/buckets/${options.bucket}/jobs/${jobId}?queue=${encodeURIComponent(options.queue)}` | ||
| }); | ||
| if (result.deleted) { | ||
| console.log(`Job ${jobId} removed.`); | ||
| } | ||
| } | ||
| export { | ||
| deleteJob, | ||
| listJobs, | ||
| retryJob | ||
| }; |
| import { | ||
| adminRequest | ||
| } from "./chunk-QF7PVPWQ.js"; | ||
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-FZGOTXTE.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/admin/jobs.ts | ||
| function formatTimestamp(ts) { | ||
| return new Date(ts).toISOString().replace("T", " ").slice(0, 19); | ||
| } | ||
| async function listAdminJobs(options) { | ||
| const params = new URLSearchParams(); | ||
| if (options.org) { | ||
| params.set("orgId", options.org); | ||
| } | ||
| if (options.bucket) { | ||
| params.set("bucketId", options.bucket); | ||
| } | ||
| if (options.status) { | ||
| params.set("status", options.status); | ||
| } | ||
| if (options.queue) { | ||
| params.set("queue", options.queue); | ||
| } | ||
| if (options.limit) { | ||
| params.set("limit", options.limit); | ||
| } | ||
| const qs = params.toString(); | ||
| const result = await adminRequest({ | ||
| method: "GET", | ||
| path: `/admin/jobs${qs ? `?${qs}` : ""}` | ||
| }); | ||
| if (result.jobs.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(result.jobs, void 0, { json: options.json }); | ||
| } else { | ||
| console.log("No jobs found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| result.jobs, | ||
| () => { | ||
| const rows = result.jobs.map((job) => [ | ||
| job.jobId, | ||
| job.queueName, | ||
| job.status, | ||
| `${job.data.fileId.slice(0, 8)}...`, | ||
| `${job.data.orgId.slice(0, 8)}...`, | ||
| formatTimestamp(job.timestamp), | ||
| job.failedReason ? job.failedReason.slice(0, 40) : "" | ||
| ]); | ||
| return formatTable( | ||
| ["ID", "Queue", "Status", "File", "Org", "Created", "Error"], | ||
| rows | ||
| ); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function retryAdminJob(jobId, options) { | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/admin/jobs/${jobId}/retry`, | ||
| body: { queue: options.queue } | ||
| }); | ||
| if (result.retried) { | ||
| console.log(`Job ${jobId} retried.`); | ||
| } | ||
| } | ||
| async function deleteAdminJob(jobId, options) { | ||
| const result = await adminRequest({ | ||
| method: "DELETE", | ||
| path: `/admin/jobs/${jobId}?queue=${encodeURIComponent(options.queue)}` | ||
| }); | ||
| if (result.deleted) { | ||
| console.log(`Job ${jobId} removed.`); | ||
| } | ||
| } | ||
| export { | ||
| deleteAdminJob, | ||
| listAdminJobs, | ||
| retryAdminJob | ||
| }; |
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-FZGOTXTE.js"; | ||
| import { | ||
| getApiKey, | ||
| getBaseUrl | ||
| } from "./chunk-TOADDO2F.js"; | ||
| // src/commands/jobs.ts | ||
| async function bucketRequest(opts) { | ||
| const baseUrl = getBaseUrl(); | ||
| const apiKey = getApiKey(); | ||
| const res = await fetch(`${baseUrl}${opts.path}`, { | ||
| method: opts.method, | ||
| headers: { | ||
| "x-api-key": apiKey, | ||
| ...opts.body ? { "Content-Type": "application/json" } : {} | ||
| }, | ||
| ...opts.body ? { body: JSON.stringify(opts.body) } : {} | ||
| }); | ||
| if (!res.ok) { | ||
| const body = await res.json().catch(() => ({ error: res.statusText })); | ||
| const message = body.error ?? `HTTP ${res.status}`; | ||
| throw new Error(message); | ||
| } | ||
| return await res.json(); | ||
| } | ||
| function formatTimestamp(ts) { | ||
| return new Date(ts).toISOString().replace("T", " ").slice(0, 19); | ||
| } | ||
| async function listJobs(bucketId, options) { | ||
| const params = new URLSearchParams(); | ||
| if (options.status) { | ||
| params.set("status", options.status); | ||
| } | ||
| if (options.queue) { | ||
| params.set("queue", options.queue); | ||
| } | ||
| if (options.limit) { | ||
| params.set("limit", options.limit); | ||
| } | ||
| const qs = params.toString(); | ||
| const path = `/buckets/${bucketId}/jobs${qs ? `?${qs}` : ""}`; | ||
| const result = await bucketRequest({ | ||
| method: "GET", | ||
| path | ||
| }); | ||
| if (result.jobs.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(result.jobs); | ||
| } else { | ||
| console.log("No jobs found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| result.jobs, | ||
| () => { | ||
| const rows = result.jobs.map((job) => [ | ||
| job.jobId, | ||
| job.queueName, | ||
| job.status, | ||
| `${job.data.fileId.slice(0, 8)}...`, | ||
| formatTimestamp(job.timestamp), | ||
| job.failedReason ? job.failedReason.slice(0, 40) : "" | ||
| ]); | ||
| return formatTable( | ||
| ["ID", "Queue", "Status", "File", "Created", "Error"], | ||
| rows | ||
| ); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function retryJob(jobId, options) { | ||
| const result = await bucketRequest({ | ||
| method: "POST", | ||
| path: `/buckets/${options.bucket}/jobs/${jobId}/retry`, | ||
| body: { queue: options.queue } | ||
| }); | ||
| if (result.retried) { | ||
| console.log(`Job ${jobId} retried.`); | ||
| } | ||
| } | ||
| async function deleteJob(jobId, options) { | ||
| const result = await bucketRequest({ | ||
| method: "DELETE", | ||
| path: `/buckets/${options.bucket}/jobs/${jobId}?queue=${encodeURIComponent(options.queue)}` | ||
| }); | ||
| if (result.deleted) { | ||
| console.log(`Job ${jobId} removed.`); | ||
| } | ||
| } | ||
| export { | ||
| deleteJob, | ||
| listJobs, | ||
| retryJob | ||
| }; |
| import { | ||
| adminRequest | ||
| } from "./chunk-QF7PVPWQ.js"; | ||
| import { | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/admin/maintenance.ts | ||
| async function cleanupDrops(options) { | ||
| const params = new URLSearchParams(); | ||
| if (options.maxAgeHours) { | ||
| params.set("maxAgeHours", options.maxAgeHours); | ||
| } | ||
| if (options.dryRun) { | ||
| params.set("dryRun", "true"); | ||
| } | ||
| const qs = params.toString(); | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/admin/cleanup-drops${qs ? `?${qs}` : ""}` | ||
| }); | ||
| output( | ||
| result, | ||
| () => options.dryRun ? `[dry run] Would clean up ${result.count ?? 0} drops` : `Cleaned up ${result.deleted ?? 0} drops`, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function purgeEvents(options) { | ||
| const params = new URLSearchParams(); | ||
| if (options.dryRun) { | ||
| params.set("dryRun", "true"); | ||
| } | ||
| const qs = params.toString(); | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/admin/purge-events${qs ? `?${qs}` : ""}` | ||
| }); | ||
| output( | ||
| result, | ||
| () => options.dryRun ? `[dry run] Would purge ${result.count ?? 0} events` : `Purged ${result.deleted ?? 0} events`, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function reconcileFiles(options) { | ||
| const params = new URLSearchParams({ bucketId: options.bucket }); | ||
| if (options.dryRun) { | ||
| params.set("dryRun", "true"); | ||
| } | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/admin/reconcile-files?${params}` | ||
| }); | ||
| output( | ||
| result, | ||
| () => { | ||
| if (options.dryRun) { | ||
| return `[dry run] ${result.mismatched ?? 0} files need reconciliation`; | ||
| } | ||
| return `Reconciled ${result.reconciled ?? 0} files`; | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function retrySyncFailures(options) { | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: "/admin/retry-sync-failures" | ||
| }); | ||
| output(result, () => `Retried ${result.retried ?? 0} sync failures`, { | ||
| json: options.json | ||
| }); | ||
| } | ||
| export { | ||
| cleanupDrops, | ||
| purgeEvents, | ||
| reconcileFiles, | ||
| retrySyncFailures | ||
| }; |
| import { | ||
| adminRequest | ||
| } from "./chunk-QF7PVPWQ.js"; | ||
| import { | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/admin/maintenance.ts | ||
| async function cleanupDrops(options) { | ||
| const params = new URLSearchParams(); | ||
| if (options.maxAgeHours) { | ||
| params.set("maxAgeHours", options.maxAgeHours); | ||
| } | ||
| if (options.dryRun) { | ||
| params.set("dryRun", "true"); | ||
| } | ||
| const qs = params.toString(); | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/admin/cleanup-drops${qs ? `?${qs}` : ""}` | ||
| }); | ||
| output( | ||
| result, | ||
| () => options.dryRun ? `[dry run] Would clean up ${result.count ?? 0} drops` : `Cleaned up ${result.deleted ?? 0} drops`, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function purgeEvents(options) { | ||
| const params = new URLSearchParams(); | ||
| if (options.dryRun) { | ||
| params.set("dryRun", "true"); | ||
| } | ||
| const qs = params.toString(); | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/admin/purge-events${qs ? `?${qs}` : ""}` | ||
| }); | ||
| output( | ||
| result, | ||
| () => options.dryRun ? `[dry run] Would purge ${result.count ?? 0} events` : `Purged ${result.deleted ?? 0} events`, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function reconcileFiles(options) { | ||
| const params = new URLSearchParams({ bucketId: options.bucket }); | ||
| if (options.dryRun) { | ||
| params.set("dryRun", "true"); | ||
| } | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/admin/reconcile-files?${params}` | ||
| }); | ||
| output( | ||
| result, | ||
| () => { | ||
| if (options.dryRun) { | ||
| return `[dry run] ${result.mismatched ?? 0} files need reconciliation`; | ||
| } | ||
| return `Reconciled ${result.reconciled ?? 0} files`; | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function retrySyncFailures(options) { | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: "/admin/retry-sync-failures" | ||
| }); | ||
| output(result, () => `Retried ${result.retried ?? 0} sync failures`, { | ||
| json: options.json | ||
| }); | ||
| } | ||
| export { | ||
| cleanupDrops, | ||
| purgeEvents, | ||
| reconcileFiles, | ||
| retrySyncFailures | ||
| }; |
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/mcp.ts | ||
| import { z } from "zod"; | ||
| async function startMcpServer() { | ||
| const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js"); | ||
| const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js"); | ||
| const server = new McpServer({ | ||
| name: "stow", | ||
| version: "2.0.4" | ||
| }); | ||
| server.tool("stow_whoami", "Show current user and organization", {}, async () => { | ||
| const stow = createStow(); | ||
| const result = await stow.whoami(); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| }); | ||
| server.tool("stow_buckets_list", "List all buckets", {}, async () => { | ||
| const stow = createStow(); | ||
| const result = await stow.listBuckets(); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| }); | ||
| server.tool( | ||
| "stow_files_list", | ||
| "List files in a bucket", | ||
| { | ||
| bucket: z.string().describe("Bucket name"), | ||
| limit: z.number().optional().describe("Max results (default 50)") | ||
| }, | ||
| async (params) => { | ||
| const stow = createStow(); | ||
| const result = await stow.listFiles({ | ||
| bucket: params.bucket, | ||
| limit: params.limit | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "stow_files_get", | ||
| "Get file details", | ||
| { | ||
| bucket: z.string().describe("Bucket name"), | ||
| key: z.string().describe("File key") | ||
| }, | ||
| async (params) => { | ||
| const stow = createStow(); | ||
| const result = await stow.getFile(params.key, { | ||
| bucket: params.bucket | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "stow_search_text", | ||
| "Search files by text query", | ||
| { | ||
| query: z.string().describe("Search query"), | ||
| bucket: z.string().optional().describe("Bucket name (optional)"), | ||
| limit: z.number().optional().describe("Max results (default 10)") | ||
| }, | ||
| async (params) => { | ||
| const stow = createStow(); | ||
| const result = await stow.search.text({ | ||
| query: params.query, | ||
| bucket: params.bucket, | ||
| limit: params.limit | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "stow_search_similar", | ||
| "Find visually similar files", | ||
| { | ||
| fileKey: z.string().describe("File key to find similar files for"), | ||
| bucket: z.string().optional().describe("Bucket name (optional)"), | ||
| limit: z.number().optional().describe("Max results (default 10)") | ||
| }, | ||
| async (params) => { | ||
| const stow = createStow(); | ||
| const result = await stow.search.similar({ | ||
| fileKey: params.fileKey, | ||
| bucket: params.bucket, | ||
| limit: params.limit | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "stow_search_color", | ||
| "Search files by hex color", | ||
| { | ||
| hex: z.string().describe("Hex color code (e.g. #ff0000)"), | ||
| bucket: z.string().optional().describe("Bucket name (optional)"), | ||
| limit: z.number().optional().describe("Max results (default 10)") | ||
| }, | ||
| async (params) => { | ||
| const stow = createStow(); | ||
| const result = await stow.search.color({ | ||
| hex: params.hex, | ||
| bucket: params.bucket, | ||
| limit: params.limit | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "stow_search_diverse", | ||
| "Get a diverse set of files from a bucket", | ||
| { | ||
| bucket: z.string().optional().describe("Bucket name (optional)"), | ||
| limit: z.number().optional().describe("Max results (default 10)") | ||
| }, | ||
| async (params) => { | ||
| const stow = createStow(); | ||
| const result = await stow.search.diverse({ | ||
| bucket: params.bucket, | ||
| limit: params.limit | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "stow_search_image", | ||
| "Search by image URL or existing file key", | ||
| { | ||
| url: z.string().optional().describe("Image URL to search by"), | ||
| fileKey: z.string().optional().describe("Existing file key to search by"), | ||
| bucket: z.string().optional().describe("Bucket name (optional)"), | ||
| limit: z.number().optional().describe("Max results (default 12)") | ||
| }, | ||
| async (params) => { | ||
| const stow = createStow(); | ||
| const input = {}; | ||
| if (params.url) { | ||
| input.url = params.url; | ||
| } | ||
| if (params.fileKey) { | ||
| input.fileKey = params.fileKey; | ||
| } | ||
| const result = await stow.search.image(input, { | ||
| bucket: params.bucket, | ||
| limit: params.limit ?? 12 | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| } | ||
| ); | ||
| server.tool("stow_tags_list", "List all tags", {}, async () => { | ||
| const stow = createStow(); | ||
| const result = await stow.tags.list(); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| }); | ||
| server.tool( | ||
| "stow_anchors_list", | ||
| "List anchors in a bucket", | ||
| { | ||
| bucket: z.string().optional().describe("Bucket name (optional)") | ||
| }, | ||
| async (params) => { | ||
| const stow = createStow(); | ||
| const result = await stow.anchors.list({ bucket: params.bucket }); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "stow_anchors_create", | ||
| "Create a text anchor for semantic search", | ||
| { | ||
| text: z.string().describe("Anchor text to embed"), | ||
| label: z.string().optional().describe("Human-readable label (optional)") | ||
| }, | ||
| async (params) => { | ||
| const stow = createStow(); | ||
| const result = await stow.anchors.create({ | ||
| text: params.text, | ||
| label: params.label | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| } | ||
| ); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| } | ||
| export { | ||
| startMcpServer | ||
| }; |
| import { | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-FZGOTXTE.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/profiles.ts | ||
| async function createProfile(options) { | ||
| const stow = createStow(); | ||
| const profile = await stow.profiles.create({ | ||
| name: options.name, | ||
| ...options.bucket ? { bucketId: options.bucket } : {} | ||
| }); | ||
| output(profile, () => `Created profile: ${profile.name} (${profile.id})`, { | ||
| json: options.json | ||
| }); | ||
| } | ||
| async function getProfile(id, options) { | ||
| const stow = createStow(); | ||
| const profile = await stow.profiles.get(id); | ||
| output( | ||
| profile, | ||
| () => { | ||
| const lines = [`Profile: ${profile.name} (${profile.id})`]; | ||
| if (profile.clusters && profile.clusters.length > 0) { | ||
| lines.push("\nClusters:"); | ||
| const rows = profile.clusters.map((c) => [ | ||
| String(c.index), | ||
| c.name ?? "(unnamed)", | ||
| String(c.signalCount) | ||
| ]); | ||
| lines.push(formatTable(["Index", "Name", "Signals"], rows)); | ||
| } | ||
| return lines.join("\n"); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function deleteProfile(id) { | ||
| const stow = createStow(); | ||
| await stow.profiles.delete(id); | ||
| console.log(`Deleted profile: ${id}`); | ||
| } | ||
| export { | ||
| createProfile, | ||
| deleteProfile, | ||
| getProfile | ||
| }; |
| import { | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/profiles.ts | ||
| async function createProfile(options) { | ||
| const stow = createStow(); | ||
| const profile = await stow.profiles.create({ | ||
| name: options.name, | ||
| ...options.bucket ? { bucketId: options.bucket } : {} | ||
| }); | ||
| output(profile, () => `Created profile: ${profile.name} (${profile.id})`, { | ||
| json: options.json | ||
| }); | ||
| } | ||
| async function getProfile(id, options) { | ||
| const stow = createStow(); | ||
| const profile = await stow.profiles.get(id); | ||
| output( | ||
| profile, | ||
| () => { | ||
| const lines = [`Profile: ${profile.name} (${profile.id})`]; | ||
| if (profile.clusters && profile.clusters.length > 0) { | ||
| lines.push("\nClusters:"); | ||
| const rows = profile.clusters.map((c) => [ | ||
| String(c.index), | ||
| c.name ?? "(unnamed)", | ||
| String(c.signalCount) | ||
| ]); | ||
| lines.push(formatTable(["Index", "Name", "Signals"], rows)); | ||
| } | ||
| return lines.join("\n"); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function deleteProfile(id) { | ||
| const stow = createStow(); | ||
| await stow.profiles.delete(id); | ||
| console.log(`Deleted profile: ${id}`); | ||
| } | ||
| export { | ||
| createProfile, | ||
| deleteProfile, | ||
| getProfile | ||
| }; |
| import { | ||
| adminRequest | ||
| } from "./chunk-QF7PVPWQ.js"; | ||
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/admin/queues.ts | ||
| async function listQueues(options) { | ||
| const result = await adminRequest({ | ||
| method: "GET", | ||
| path: "/admin/queues" | ||
| }); | ||
| const entries = Object.entries(result.queues); | ||
| if (entries.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(result.queues, void 0, { json: options.json }); | ||
| } else { | ||
| console.log("No queues found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| result.queues, | ||
| () => { | ||
| const rows = entries.map(([name, counts]) => [ | ||
| name, | ||
| String(counts.waiting), | ||
| String(counts.active), | ||
| String(counts.completed), | ||
| String(counts.failed) | ||
| ]); | ||
| return formatTable(["Queue", "Waiting", "Active", "Completed", "Failed"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function cleanQueue(queueName, options) { | ||
| const status = options.failed ? "failed" : "completed"; | ||
| const grace = options.grace ? Number(options.grace) : 0; | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/admin/queues/${encodeURIComponent(queueName)}/clean`, | ||
| body: { status, grace } | ||
| }); | ||
| console.log(`Cleaned ${result.cleaned} ${result.status} jobs from ${result.queue}.`); | ||
| } | ||
| export { | ||
| cleanQueue, | ||
| listQueues | ||
| }; |
| import { | ||
| adminRequest | ||
| } from "./chunk-QF7PVPWQ.js"; | ||
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-FZGOTXTE.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/admin/queues.ts | ||
| async function listQueues(options) { | ||
| const result = await adminRequest({ | ||
| method: "GET", | ||
| path: "/admin/queues" | ||
| }); | ||
| const entries = Object.entries(result.queues); | ||
| if (entries.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(result.queues, void 0, { json: options.json }); | ||
| } else { | ||
| console.log("No queues found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| result.queues, | ||
| () => { | ||
| const rows = entries.map(([name, counts]) => [ | ||
| name, | ||
| String(counts.waiting), | ||
| String(counts.active), | ||
| String(counts.completed), | ||
| String(counts.failed) | ||
| ]); | ||
| return formatTable( | ||
| ["Queue", "Waiting", "Active", "Completed", "Failed"], | ||
| rows | ||
| ); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function cleanQueue(queueName, options) { | ||
| const status = options.failed ? "failed" : "completed"; | ||
| const grace = options.grace ? Number(options.grace) : 0; | ||
| const result = await adminRequest({ | ||
| method: "POST", | ||
| path: `/admin/queues/${encodeURIComponent(queueName)}/clean`, | ||
| body: { status, grace } | ||
| }); | ||
| console.log( | ||
| `Cleaned ${result.cleaned} ${result.status} jobs from ${result.queue}.` | ||
| ); | ||
| } | ||
| export { | ||
| cleanQueue, | ||
| listQueues | ||
| }; |
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import { | ||
| formatBytes, | ||
| formatTable | ||
| } from "./chunk-FZGOTXTE.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/search.ts | ||
| async function textSearch(query, options) { | ||
| const stow = createStow(); | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const data = await stow.search.text({ | ||
| query, | ||
| ...options.bucket ? { bucket: options.bucket } : {}, | ||
| ...parsedLimit && parsedLimit > 0 ? { limit: parsedLimit } : {} | ||
| }); | ||
| if (data.results.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log("No results found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| data, | ||
| () => { | ||
| const rows = data.results.map((r) => [ | ||
| r.key, | ||
| formatBytes(r.size), | ||
| r.similarity.toFixed(3) | ||
| ]); | ||
| return formatTable(["Key", "Size", "Similarity"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function similarSearch(options) { | ||
| const stow = createStow(); | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const data = await stow.search.similar({ | ||
| fileKey: options.file, | ||
| ...options.bucket ? { bucket: options.bucket } : {}, | ||
| ...parsedLimit && parsedLimit > 0 ? { limit: parsedLimit } : {} | ||
| }); | ||
| if (data.results.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log("No similar files found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| data, | ||
| () => { | ||
| const rows = data.results.map((r) => [ | ||
| r.key, | ||
| formatBytes(r.size), | ||
| r.similarity.toFixed(3) | ||
| ]); | ||
| return formatTable(["Key", "Size", "Similarity"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function colorSearch(options) { | ||
| const stow = createStow(); | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const data = await stow.search.color({ | ||
| hex: options.hex, | ||
| ...options.bucket ? { bucket: options.bucket } : {}, | ||
| ...parsedLimit && parsedLimit > 0 ? { limit: parsedLimit } : {} | ||
| }); | ||
| if (data.results.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log("No results found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| data, | ||
| () => { | ||
| const rows = data.results.map((r) => [ | ||
| r.key, | ||
| r.contentType, | ||
| r.colorDistance.toFixed(3) | ||
| ]); | ||
| return formatTable(["Key", "Type", "Distance"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function diverseSearch(options) { | ||
| const stow = createStow(); | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const data = await stow.search.diverse({ | ||
| ...options.bucket ? { bucket: options.bucket } : {}, | ||
| ...parsedLimit && parsedLimit > 0 ? { limit: parsedLimit } : {} | ||
| }); | ||
| if (data.results.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log("No results found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| data, | ||
| () => { | ||
| const rows = data.results.map((r) => [ | ||
| r.key, | ||
| formatBytes(r.size), | ||
| r.similarity.toFixed(3) | ||
| ]); | ||
| return formatTable(["Key", "Size", "Similarity"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| export { | ||
| colorSearch, | ||
| diverseSearch, | ||
| similarSearch, | ||
| textSearch | ||
| }; |
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import { | ||
| formatBytes, | ||
| formatTable | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/search.ts | ||
| async function textSearch(query, options) { | ||
| const stow = createStow(); | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const data = await stow.search.text({ | ||
| query, | ||
| ...options.bucket ? { bucket: options.bucket } : {}, | ||
| ...parsedLimit && parsedLimit > 0 ? { limit: parsedLimit } : {} | ||
| }); | ||
| if (data.results.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log("No results found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| data, | ||
| () => { | ||
| const rows = data.results.map((r) => [r.key, formatBytes(r.size), r.similarity.toFixed(3)]); | ||
| return formatTable(["Key", "Size", "Similarity"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function similarSearch(options) { | ||
| const stow = createStow(); | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const data = await stow.search.similar({ | ||
| fileKey: options.file, | ||
| ...options.bucket ? { bucket: options.bucket } : {}, | ||
| ...parsedLimit && parsedLimit > 0 ? { limit: parsedLimit } : {} | ||
| }); | ||
| if (data.results.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log("No similar files found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| data, | ||
| () => { | ||
| const rows = data.results.map((r) => [r.key, formatBytes(r.size), r.similarity.toFixed(3)]); | ||
| return formatTable(["Key", "Size", "Similarity"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function colorSearch(options) { | ||
| const stow = createStow(); | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const data = await stow.search.color({ | ||
| hex: options.hex, | ||
| ...options.bucket ? { bucket: options.bucket } : {}, | ||
| ...parsedLimit && parsedLimit > 0 ? { limit: parsedLimit } : {} | ||
| }); | ||
| if (data.results.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log("No results found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| data, | ||
| () => { | ||
| const rows = data.results.map((r) => [r.key, r.contentType, r.colorDistance.toFixed(3)]); | ||
| return formatTable(["Key", "Type", "Distance"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| async function diverseSearch(options) { | ||
| const stow = createStow(); | ||
| const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null; | ||
| const data = await stow.search.diverse({ | ||
| ...options.bucket ? { bucket: options.bucket } : {}, | ||
| ...parsedLimit && parsedLimit > 0 ? { limit: parsedLimit } : {} | ||
| }); | ||
| if (data.results.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log("No results found."); | ||
| } | ||
| return; | ||
| } | ||
| output( | ||
| data, | ||
| () => { | ||
| const rows = data.results.map((r) => [r.key, formatBytes(r.size), r.similarity.toFixed(3)]); | ||
| return formatTable(["Key", "Size", "Similarity"], rows); | ||
| }, | ||
| { json: options.json } | ||
| ); | ||
| } | ||
| export { | ||
| colorSearch, | ||
| diverseSearch, | ||
| similarSearch, | ||
| textSearch | ||
| }; |
| import { | ||
| parseJsonInput | ||
| } from "./chunk-AHBVZRDR.js"; | ||
| import { | ||
| validateInput | ||
| } from "./chunk-533UGNLM.js"; | ||
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-RH4BOSYB.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-FZGOTXTE.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/tags.ts | ||
| async function listTags(options) { | ||
| const stow = createStow(); | ||
| const data = await stow.tags.list(); | ||
| if (data.tags.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log("No tags yet. Create one with: stow tags create <name>"); | ||
| } | ||
| return; | ||
| } | ||
| output(data, () => { | ||
| const rows = data.tags.map((t) => [t.name, t.slug, t.color ?? "\u2014", t.id]); | ||
| return formatTable(["Name", "Slug", "Color", "ID"], rows); | ||
| }); | ||
| } | ||
| async function createTag(name, options) { | ||
| const input = parseJsonInput( | ||
| options.inputJson, | ||
| { name, color: options.color } | ||
| ); | ||
| validateInput(input.name, "tag name"); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "createTag", | ||
| details: { | ||
| name: input.name, | ||
| color: input.color ?? null | ||
| } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| const tag = await stow.tags.create({ | ||
| name: input.name, | ||
| ...input.color ? { color: input.color } : {} | ||
| }); | ||
| output(tag, () => `Created tag: ${tag.name}`); | ||
| } | ||
| async function deleteTag(id, options = {}) { | ||
| validateInput(id, "tag id"); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "deleteTag", | ||
| details: { id } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| await stow.tags.delete(id); | ||
| console.log(`Deleted tag: ${id}`); | ||
| } | ||
| export { | ||
| createTag, | ||
| deleteTag, | ||
| listTags | ||
| }; |
| import { | ||
| parseJsonInput | ||
| } from "./chunk-XVKIRHTX.js"; | ||
| import { | ||
| validateInput | ||
| } from "./chunk-NBHBVKP5.js"; | ||
| import { | ||
| isJsonOutput, | ||
| output | ||
| } from "./chunk-KPIQZBTO.js"; | ||
| import { | ||
| formatTable | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/tags.ts | ||
| async function listTags(options) { | ||
| const stow = createStow(); | ||
| const data = await stow.tags.list(); | ||
| if (data.tags.length === 0) { | ||
| if (isJsonOutput() || options.json) { | ||
| output(data); | ||
| } else { | ||
| console.log("No tags yet. Create one with: stow tags create <name>"); | ||
| } | ||
| return; | ||
| } | ||
| output(data, () => { | ||
| const rows = data.tags.map((t) => [t.name, t.slug, t.color ?? "\u2014", t.id]); | ||
| return formatTable(["Name", "Slug", "Color", "ID"], rows); | ||
| }); | ||
| } | ||
| async function createTag(name, options) { | ||
| const input = parseJsonInput(options.inputJson, { | ||
| name, | ||
| color: options.color | ||
| }); | ||
| validateInput(input.name, "tag name"); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "createTag", | ||
| details: { | ||
| name: input.name, | ||
| color: input.color ?? null | ||
| } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| const tag = await stow.tags.create({ | ||
| name: input.name, | ||
| ...input.color ? { color: input.color } : {} | ||
| }); | ||
| output(tag, () => `Created tag: ${tag.name}`); | ||
| } | ||
| async function deleteTag(id, options = {}) { | ||
| validateInput(id, "tag id"); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "deleteTag", | ||
| details: { id } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const stow = createStow(); | ||
| await stow.tags.delete(id); | ||
| console.log(`Deleted tag: ${id}`); | ||
| } | ||
| export { | ||
| createTag, | ||
| deleteTag, | ||
| listTags | ||
| }; |
| import { | ||
| validateBucketName | ||
| } from "./chunk-533UGNLM.js"; | ||
| import { | ||
| formatBytes | ||
| } from "./chunk-FZGOTXTE.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/upload.ts | ||
| import { existsSync, readFileSync, statSync } from "fs"; | ||
| import { basename, resolve } from "path"; | ||
| var CONTENT_TYPES = { | ||
| png: "image/png", | ||
| jpg: "image/jpeg", | ||
| jpeg: "image/jpeg", | ||
| gif: "image/gif", | ||
| webp: "image/webp", | ||
| svg: "image/svg+xml", | ||
| ico: "image/x-icon", | ||
| avif: "image/avif", | ||
| pdf: "application/pdf", | ||
| mp4: "video/mp4", | ||
| webm: "video/webm", | ||
| mov: "video/quicktime", | ||
| mp3: "audio/mpeg", | ||
| wav: "audio/wav", | ||
| ogg: "audio/ogg", | ||
| zip: "application/zip", | ||
| tar: "application/x-tar", | ||
| gz: "application/gzip", | ||
| txt: "text/plain", | ||
| json: "application/json", | ||
| xml: "application/xml", | ||
| html: "text/html", | ||
| css: "text/css", | ||
| js: "application/javascript" | ||
| }; | ||
| function getContentType(filename) { | ||
| const ext = filename.toLowerCase().split(".").pop(); | ||
| return CONTENT_TYPES[ext || ""] || "application/octet-stream"; | ||
| } | ||
| function readFile(filePath) { | ||
| const resolvedPath = resolve(filePath); | ||
| if (!existsSync(resolvedPath)) { | ||
| console.error(`Error: File not found: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
| const buffer = readFileSync(resolvedPath); | ||
| const filename = basename(resolvedPath); | ||
| const contentType = getContentType(filename); | ||
| return { buffer, filename, contentType }; | ||
| } | ||
| function printUploadResult(url, options, opts) { | ||
| if (options.quiet) { | ||
| console.log(url); | ||
| return; | ||
| } | ||
| console.error(opts?.deduped ? "Done! (deduped)" : "Done!"); | ||
| console.log(""); | ||
| console.log(url); | ||
| } | ||
| async function uploadDrop(filePath, options) { | ||
| const { buffer, filename, contentType } = readFile(filePath); | ||
| if (!options.quiet) { | ||
| console.error(`Uploading ${filename} (${formatBytes(buffer.length)})...`); | ||
| } | ||
| const stow = createStow(); | ||
| const result = await stow.drop(buffer, { | ||
| filename, | ||
| contentType | ||
| }); | ||
| printUploadResult(result.url, options); | ||
| } | ||
| async function uploadFile(filePath, options) { | ||
| if (options.bucket) { | ||
| validateBucketName(options.bucket); | ||
| } | ||
| const resolvedPath = resolve(filePath); | ||
| if (!existsSync(resolvedPath)) { | ||
| console.error(`Error: File not found: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
| const filename = basename(resolvedPath); | ||
| const contentType = getContentType(filename); | ||
| const size = statSync(resolvedPath).size; | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "upload", | ||
| details: { | ||
| file: resolvedPath, | ||
| filename, | ||
| contentType, | ||
| size, | ||
| bucket: options.bucket ?? null | ||
| } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const buffer = readFileSync(resolvedPath); | ||
| if (!options.quiet) { | ||
| console.error(`Uploading ${filename} (${formatBytes(buffer.length)})...`); | ||
| } | ||
| const stow = createStow(); | ||
| const result = await stow.uploadFile(buffer, { | ||
| ...options.bucket ? { bucket: options.bucket } : {}, | ||
| filename, | ||
| contentType | ||
| }); | ||
| printUploadResult(result.url ?? result.key, options, { | ||
| deduped: result.deduped | ||
| }); | ||
| } | ||
| export { | ||
| uploadDrop, | ||
| uploadFile | ||
| }; |
| import { | ||
| validateBucketName | ||
| } from "./chunk-NBHBVKP5.js"; | ||
| import { | ||
| formatBytes | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/upload.ts | ||
| import { existsSync, readFileSync, statSync } from "fs"; | ||
| import { basename, resolve } from "path"; | ||
| var CONTENT_TYPES = { | ||
| png: "image/png", | ||
| jpg: "image/jpeg", | ||
| jpeg: "image/jpeg", | ||
| gif: "image/gif", | ||
| webp: "image/webp", | ||
| svg: "image/svg+xml", | ||
| ico: "image/x-icon", | ||
| avif: "image/avif", | ||
| pdf: "application/pdf", | ||
| mp4: "video/mp4", | ||
| webm: "video/webm", | ||
| mov: "video/quicktime", | ||
| mp3: "audio/mpeg", | ||
| wav: "audio/wav", | ||
| ogg: "audio/ogg", | ||
| zip: "application/zip", | ||
| tar: "application/x-tar", | ||
| gz: "application/gzip", | ||
| txt: "text/plain", | ||
| json: "application/json", | ||
| xml: "application/xml", | ||
| html: "text/html", | ||
| css: "text/css", | ||
| js: "application/javascript" | ||
| }; | ||
| function getContentType(filename) { | ||
| const ext = filename.toLowerCase().split(".").pop(); | ||
| return CONTENT_TYPES[ext || ""] || "application/octet-stream"; | ||
| } | ||
| function readFile(filePath) { | ||
| const resolvedPath = resolve(filePath); | ||
| if (!existsSync(resolvedPath)) { | ||
| console.error(`Error: File not found: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
| const buffer = readFileSync(resolvedPath); | ||
| const filename = basename(resolvedPath); | ||
| const contentType = getContentType(filename); | ||
| return { buffer, filename, contentType }; | ||
| } | ||
| function printUploadResult(url, options, opts) { | ||
| if (options.quiet) { | ||
| console.log(url); | ||
| return; | ||
| } | ||
| console.error(opts?.deduped ? "Done! (deduped)" : "Done!"); | ||
| console.log(""); | ||
| console.log(url); | ||
| } | ||
| async function uploadDrop(filePath, options) { | ||
| const { buffer, filename, contentType } = readFile(filePath); | ||
| if (!options.quiet) { | ||
| console.error(`Uploading ${filename} (${formatBytes(buffer.length)})...`); | ||
| } | ||
| const stow = createStow(); | ||
| const result = await stow.drop(buffer, { | ||
| filename, | ||
| contentType | ||
| }); | ||
| printUploadResult(result.url, options); | ||
| } | ||
| async function uploadFile(filePath, options) { | ||
| if (options.bucket) { | ||
| validateBucketName(options.bucket); | ||
| } | ||
| const resolvedPath = resolve(filePath); | ||
| if (!existsSync(resolvedPath)) { | ||
| console.error(`Error: File not found: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
| const filename = basename(resolvedPath); | ||
| const contentType = getContentType(filename); | ||
| const { size } = statSync(resolvedPath); | ||
| if (options.dryRun) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| dryRun: true, | ||
| action: "upload", | ||
| details: { | ||
| file: resolvedPath, | ||
| filename, | ||
| contentType, | ||
| size, | ||
| bucket: options.bucket ?? null | ||
| } | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| const buffer = readFileSync(resolvedPath); | ||
| if (!options.quiet) { | ||
| console.error(`Uploading ${filename} (${formatBytes(buffer.length)})...`); | ||
| } | ||
| const stow = createStow(); | ||
| const result = await stow.uploadFile(buffer, { | ||
| ...options.bucket ? { bucket: options.bucket } : {}, | ||
| filename, | ||
| contentType | ||
| }); | ||
| printUploadResult(result.url ?? result.key, options, { | ||
| deduped: result.deduped | ||
| }); | ||
| } | ||
| export { | ||
| uploadDrop, | ||
| uploadFile | ||
| }; |
| import { | ||
| formatBytes | ||
| } from "./chunk-PE6V3MVP.js"; | ||
| import { | ||
| createStow | ||
| } from "./chunk-5LU25QZK.js"; | ||
| import "./chunk-TOADDO2F.js"; | ||
| // src/commands/whoami.ts | ||
| async function whoami() { | ||
| const stow = createStow(); | ||
| const data = await stow.whoami(); | ||
| console.log(`Account: ${data.user.email}`); | ||
| console.log(""); | ||
| console.log(`Buckets: ${data.stats.bucketCount}`); | ||
| console.log(`Files: ${data.stats.totalFiles}`); | ||
| console.log(`Storage: ${formatBytes(data.stats.totalBytes)}`); | ||
| if (data.key) { | ||
| console.log(""); | ||
| console.log(`API Key: ${data.key.name}`); | ||
| console.log(`Scope: ${data.key.scope}`); | ||
| const perms = Object.entries(data.key.permissions).filter(([, v]) => v).map(([k]) => k); | ||
| console.log(`Perms: ${perms.join(", ")}`); | ||
| } | ||
| } | ||
| export { | ||
| whoami | ||
| }; |
+181
-199
@@ -5,6 +5,6 @@ #!/usr/bin/env node | ||
| renderCommandHelp | ||
| } from "./chunk-XJDK2CBE.js"; | ||
| } from "./chunk-MYFLRBWC.js"; | ||
| import { | ||
| InputValidationError | ||
| } from "./chunk-PLZFHPLC.js"; | ||
| } from "./chunk-NBHBVKP5.js"; | ||
| import { | ||
@@ -15,3 +15,3 @@ outputError, | ||
| setGlobalNdjson | ||
| } from "./chunk-5IX3ASXH.js"; | ||
| } from "./chunk-KPIQZBTO.js"; | ||
@@ -36,6 +36,3 @@ // src/cli.ts | ||
| var program = new Command(); | ||
| program.name("stow").description(CLI_DOCS.root.description).version(VERSION).option("--human", "Force human-readable output (default when TTY)").option( | ||
| "--fields <fields>", | ||
| "Comma-separated fields to include in output (e.g. key,similarity)" | ||
| ).option("--ndjson", "Output as newline-delimited JSON (one object per line)").addHelpText("after", renderCommandHelp("root")); | ||
| program.name("stow").description(CLI_DOCS.root.description).version(VERSION).option("--human", "Force human-readable output (default when TTY)").option("--fields <fields>", "Comma-separated fields to include in output (e.g. key,similarity)").option("--ndjson", "Output as newline-delimited JSON (one object per line)").addHelpText("after", renderCommandHelp("root")); | ||
| program.hook("preAction", () => { | ||
@@ -55,24 +52,22 @@ const opts2 = program.opts(); | ||
| try { | ||
| const { uploadDrop } = await import("./upload-OS6Q6LW5.js"); | ||
| const { uploadDrop } = await import("./upload-N7NAVN3Q.js"); | ||
| await uploadDrop(file, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| }); | ||
| 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").option("--dry-run", "Preview without uploading").addHelpText("after", renderCommandHelp("upload")).action( | ||
| async (file, options) => { | ||
| try { | ||
| const { uploadFile } = await import("./upload-OS6Q6LW5.js"); | ||
| await uploadFile(file, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } | ||
| 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").option("--dry-run", "Preview without uploading").addHelpText("after", renderCommandHelp("upload")).action(async (file, options) => { | ||
| try { | ||
| const { uploadFile } = await import("./upload-N7NAVN3Q.js"); | ||
| await uploadFile(file, options); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| ); | ||
| }); | ||
| var bucketsCmd = program.command("buckets").description(CLI_DOCS.buckets.description).addHelpText("after", renderCommandHelp("buckets")).action(async () => { | ||
| try { | ||
| const { listBuckets } = await import("./buckets-AFNX7FV3.js"); | ||
| const { listBuckets } = await import("./buckets-FPMMPRR2.js"); | ||
| await listBuckets(); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -83,6 +78,6 @@ }); | ||
| try { | ||
| const { createBucket } = await import("./buckets-AFNX7FV3.js"); | ||
| const { createBucket } = await import("./buckets-FPMMPRR2.js"); | ||
| await createBucket(name, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -94,6 +89,6 @@ } | ||
| try { | ||
| const { renameBucket } = await import("./buckets-AFNX7FV3.js"); | ||
| const { renameBucket } = await import("./buckets-FPMMPRR2.js"); | ||
| await renameBucket(name, newName, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -104,6 +99,6 @@ } | ||
| try { | ||
| const { deleteBucket } = await import("./buckets-AFNX7FV3.js"); | ||
| const { deleteBucket } = await import("./buckets-FPMMPRR2.js"); | ||
| await deleteBucket(id, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -118,6 +113,6 @@ }); | ||
| try { | ||
| const { listFiles } = await import("./files-XU6MDPP4.js"); | ||
| const { listFiles } = await import("./files-SQURZ7VO.js"); | ||
| await listFiles(bucket, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -128,6 +123,6 @@ } | ||
| try { | ||
| const { getFile } = await import("./files-XU6MDPP4.js"); | ||
| const { getFile } = await import("./files-SQURZ7VO.js"); | ||
| await getFile(bucket, key, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -138,6 +133,6 @@ }); | ||
| try { | ||
| const { updateFile } = await import("./files-XU6MDPP4.js"); | ||
| const { updateFile } = await import("./files-SQURZ7VO.js"); | ||
| await updateFile(bucket, key, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -148,24 +143,22 @@ } | ||
| try { | ||
| const { enrichFile } = await import("./files-XU6MDPP4.js"); | ||
| const { enrichFile } = await import("./files-SQURZ7VO.js"); | ||
| await enrichFile(bucket, key); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| }); | ||
| filesCmd.command("missing").description(CLI_DOCS.filesMissing.description).argument("<bucket>", "Bucket name").argument("<type>", "dimensions | embeddings | colors").option("-l, --limit <count>", "Max files to return").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("filesMissing")).action( | ||
| async (bucket, type, options) => { | ||
| try { | ||
| const { listMissing } = await import("./files-XU6MDPP4.js"); | ||
| await listMissing(bucket, type, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } | ||
| filesCmd.command("missing").description(CLI_DOCS.filesMissing.description).argument("<bucket>", "Bucket name").argument("<type>", "dimensions | embeddings | colors").option("-l, --limit <count>", "Max files to return").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("filesMissing")).action(async (bucket, type, options) => { | ||
| try { | ||
| const { listMissing } = await import("./files-SQURZ7VO.js"); | ||
| await listMissing(bucket, type, options); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| ); | ||
| }); | ||
| var dropsCmd = program.command("drops").description(CLI_DOCS.drops.description).addHelpText("after", renderCommandHelp("drops")).action(async () => { | ||
| try { | ||
| const { listDrops } = await import("./drops-5VIEW3XZ.js"); | ||
| const { listDrops } = await import("./drops-XO4CZ4BH.js"); | ||
| await listDrops(); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -175,55 +168,47 @@ }); | ||
| try { | ||
| const { deleteDrop } = await import("./drops-5VIEW3XZ.js"); | ||
| const { deleteDrop } = await import("./drops-XO4CZ4BH.js"); | ||
| await deleteDrop(id); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| }); | ||
| var searchCmd = program.command("search").description(CLI_DOCS.search.description).addHelpText("after", renderCommandHelp("search")); | ||
| searchCmd.command("text").description(CLI_DOCS.searchText.description).argument("<query>", "Search query").option("-b, --bucket <name>", "Bucket name").option("-l, --limit <count>", "Max results").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("searchText")).action( | ||
| async (query, options) => { | ||
| try { | ||
| const { textSearch } = await import("./search-ETC2EXKM.js"); | ||
| await textSearch(query, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } | ||
| searchCmd.command("text").description(CLI_DOCS.searchText.description).argument("<query>", "Search query").option("-b, --bucket <name>", "Bucket name").option("-l, --limit <count>", "Max results").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("searchText")).action(async (query, options) => { | ||
| try { | ||
| const { textSearch } = await import("./search-UWLK4OL2.js"); | ||
| await textSearch(query, options); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| ); | ||
| searchCmd.command("similar").description(CLI_DOCS.searchSimilar.description).requiredOption("--file <key>", "File key to search from").option("-b, --bucket <name>", "Bucket name").option("-l, --limit <count>", "Max results").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("searchSimilar")).action( | ||
| async (options) => { | ||
| try { | ||
| const { similarSearch } = await import("./search-ETC2EXKM.js"); | ||
| await similarSearch(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } | ||
| }); | ||
| searchCmd.command("similar").description(CLI_DOCS.searchSimilar.description).requiredOption("--file <key>", "File key to search from").option("-b, --bucket <name>", "Bucket name").option("-l, --limit <count>", "Max results").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("searchSimilar")).action(async (options) => { | ||
| try { | ||
| const { similarSearch } = await import("./search-UWLK4OL2.js"); | ||
| await similarSearch(options); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| ); | ||
| searchCmd.command("color").description(CLI_DOCS.searchColor.description).requiredOption("--hex <color>", "Hex color code").option("-b, --bucket <name>", "Bucket name").option("-l, --limit <count>", "Max results").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("searchColor")).action( | ||
| async (options) => { | ||
| try { | ||
| const { colorSearch } = await import("./search-ETC2EXKM.js"); | ||
| await colorSearch(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } | ||
| }); | ||
| searchCmd.command("color").description(CLI_DOCS.searchColor.description).requiredOption("--hex <color>", "Hex color code").option("-b, --bucket <name>", "Bucket name").option("-l, --limit <count>", "Max results").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("searchColor")).action(async (options) => { | ||
| try { | ||
| const { colorSearch } = await import("./search-UWLK4OL2.js"); | ||
| await colorSearch(options); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| ); | ||
| searchCmd.command("diverse").description(CLI_DOCS.searchDiverse.description).option("-b, --bucket <name>", "Bucket name").option("-l, --limit <count>", "Max results").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("searchDiverse")).action( | ||
| async (options) => { | ||
| try { | ||
| const { diverseSearch } = await import("./search-ETC2EXKM.js"); | ||
| await diverseSearch(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } | ||
| }); | ||
| searchCmd.command("diverse").description(CLI_DOCS.searchDiverse.description).option("-b, --bucket <name>", "Bucket name").option("-l, --limit <count>", "Max results").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("searchDiverse")).action(async (options) => { | ||
| try { | ||
| const { diverseSearch } = await import("./search-UWLK4OL2.js"); | ||
| await diverseSearch(options); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| ); | ||
| }); | ||
| var tagsCmd = program.command("tags").description(CLI_DOCS.tags.description).option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("tags")).action(async (options) => { | ||
| try { | ||
| const { listTags } = await import("./tags-TBFPDHIQ.js"); | ||
| const { listTags } = await import("./tags-V43DCLPQ.js"); | ||
| await listTags(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -234,6 +219,6 @@ }); | ||
| try { | ||
| const { createTag } = await import("./tags-TBFPDHIQ.js"); | ||
| const { createTag } = await import("./tags-V43DCLPQ.js"); | ||
| await createTag(name, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -244,25 +229,23 @@ } | ||
| try { | ||
| const { deleteTag } = await import("./tags-TBFPDHIQ.js"); | ||
| const { deleteTag } = await import("./tags-V43DCLPQ.js"); | ||
| await deleteTag(id, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| }); | ||
| var profilesCmd = program.command("profiles").description(CLI_DOCS.profiles.description).addHelpText("after", renderCommandHelp("profiles")); | ||
| profilesCmd.command("create").description(CLI_DOCS.profilesCreate.description).requiredOption("--name <name>", "Profile name").option("-b, --bucket <id>", "Bucket ID").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("profilesCreate")).action( | ||
| async (options) => { | ||
| try { | ||
| const { createProfile } = await import("./profiles-MB3TZQE4.js"); | ||
| await createProfile(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } | ||
| profilesCmd.command("create").description(CLI_DOCS.profilesCreate.description).requiredOption("--name <name>", "Profile name").option("-b, --bucket <id>", "Bucket ID").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("profilesCreate")).action(async (options) => { | ||
| try { | ||
| const { createProfile } = await import("./profiles-XXVM3UKI.js"); | ||
| await createProfile(options); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| ); | ||
| }); | ||
| profilesCmd.command("get").description(CLI_DOCS.profilesGet.description).argument("<id>", "Profile ID").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("profilesGet")).action(async (id, options) => { | ||
| try { | ||
| const { getProfile } = await import("./profiles-MB3TZQE4.js"); | ||
| const { getProfile } = await import("./profiles-XXVM3UKI.js"); | ||
| await getProfile(id, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -272,6 +255,6 @@ }); | ||
| try { | ||
| const { deleteProfile } = await import("./profiles-MB3TZQE4.js"); | ||
| const { deleteProfile } = await import("./profiles-XXVM3UKI.js"); | ||
| await deleteProfile(id); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -282,6 +265,6 @@ }); | ||
| try { | ||
| const { listJobs } = await import("./jobs-TND5AHCL.js"); | ||
| const { listJobs } = await import("./jobs-KK5IZYO5.js"); | ||
| await listJobs(options.bucket, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -292,6 +275,6 @@ } | ||
| try { | ||
| const { retryJob } = await import("./jobs-TND5AHCL.js"); | ||
| const { retryJob } = await import("./jobs-KK5IZYO5.js"); | ||
| await retryJob(id, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -301,6 +284,6 @@ }); | ||
| try { | ||
| const { deleteJob } = await import("./jobs-TND5AHCL.js"); | ||
| const { deleteJob } = await import("./jobs-KK5IZYO5.js"); | ||
| await deleteJob(id, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -311,6 +294,6 @@ }); | ||
| try { | ||
| const { health } = await import("./health-SH6T6DZS.js"); | ||
| const { health } = await import("./health-3U3RHXFS.js"); | ||
| await health(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -322,6 +305,6 @@ }); | ||
| try { | ||
| const { backfillDimensions } = await import("./backfill-VAORMLMY.js"); | ||
| const { backfillDimensions } = await import("./backfill-BG65X4TP.js"); | ||
| await backfillDimensions(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -333,6 +316,6 @@ } | ||
| try { | ||
| const { backfillColors } = await import("./backfill-VAORMLMY.js"); | ||
| const { backfillColors } = await import("./backfill-BG65X4TP.js"); | ||
| await backfillColors(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -344,43 +327,39 @@ } | ||
| try { | ||
| const { backfillEmbeddings } = await import("./backfill-VAORMLMY.js"); | ||
| const { backfillEmbeddings } = await import("./backfill-BG65X4TP.js"); | ||
| await backfillEmbeddings(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| } | ||
| ); | ||
| adminCmd.command("cleanup-drops").description(CLI_DOCS.adminCleanupDrops.description).option("--max-age-hours <hours>", "Max age in hours").option("--dry-run", "Preview without deleting").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("adminCleanupDrops")).action( | ||
| async (options) => { | ||
| try { | ||
| const { cleanupDrops } = await import("./maintenance-V2TXPXQE.js"); | ||
| await cleanupDrops(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } | ||
| adminCmd.command("cleanup-drops").description(CLI_DOCS.adminCleanupDrops.description).option("--max-age-hours <hours>", "Max age in hours").option("--dry-run", "Preview without deleting").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("adminCleanupDrops")).action(async (options) => { | ||
| try { | ||
| const { cleanupDrops } = await import("./maintenance-7UBKZOR3.js"); | ||
| await cleanupDrops(options); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| ); | ||
| }); | ||
| adminCmd.command("purge-events").description(CLI_DOCS.adminPurgeEvents.description).option("--dry-run", "Preview without deleting").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("adminPurgeEvents")).action(async (options) => { | ||
| try { | ||
| const { purgeEvents } = await import("./maintenance-V2TXPXQE.js"); | ||
| const { purgeEvents } = await import("./maintenance-7UBKZOR3.js"); | ||
| await purgeEvents(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| }); | ||
| adminCmd.command("reconcile-files").description(CLI_DOCS.adminReconcileFiles.description).requiredOption("--bucket <id>", "Bucket ID").option("--dry-run", "Preview without reconciling").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("adminReconcileFiles")).action( | ||
| async (options) => { | ||
| try { | ||
| const { reconcileFiles } = await import("./maintenance-V2TXPXQE.js"); | ||
| await reconcileFiles(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } | ||
| adminCmd.command("reconcile-files").description(CLI_DOCS.adminReconcileFiles.description).requiredOption("--bucket <id>", "Bucket ID").option("--dry-run", "Preview without reconciling").option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("adminReconcileFiles")).action(async (options) => { | ||
| try { | ||
| const { reconcileFiles } = await import("./maintenance-7UBKZOR3.js"); | ||
| await reconcileFiles(options); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| ); | ||
| }); | ||
| adminCmd.command("retry-sync-failures").description(CLI_DOCS.adminRetrySyncFailures.description).option("--json", "Output as JSON").addHelpText("after", renderCommandHelp("adminRetrySyncFailures")).action(async (options) => { | ||
| try { | ||
| const { retrySyncFailures } = await import("./maintenance-V2TXPXQE.js"); | ||
| const { retrySyncFailures } = await import("./maintenance-7UBKZOR3.js"); | ||
| await retrySyncFailures(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -391,6 +370,6 @@ }); | ||
| try { | ||
| const { listAdminJobs } = await import("./jobs-ROJFRPMR.js"); | ||
| const { listAdminJobs } = await import("./jobs-HUW6Z6A7.js"); | ||
| await listAdminJobs(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -401,6 +380,6 @@ } | ||
| try { | ||
| const { retryAdminJob } = await import("./jobs-ROJFRPMR.js"); | ||
| const { retryAdminJob } = await import("./jobs-HUW6Z6A7.js"); | ||
| await retryAdminJob(id, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -410,6 +389,6 @@ }); | ||
| try { | ||
| const { deleteAdminJob } = await import("./jobs-ROJFRPMR.js"); | ||
| const { deleteAdminJob } = await import("./jobs-HUW6Z6A7.js"); | ||
| await deleteAdminJob(id, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -419,6 +398,6 @@ }); | ||
| try { | ||
| const { listQueues } = await import("./queues-NR25TGT7.js"); | ||
| const { listQueues } = await import("./queues-MTA2RWUP.js"); | ||
| await listQueues(options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -429,25 +408,23 @@ }); | ||
| try { | ||
| const { cleanQueue } = await import("./queues-NR25TGT7.js"); | ||
| const { cleanQueue } = await import("./queues-MTA2RWUP.js"); | ||
| await cleanQueue(name, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| } | ||
| ); | ||
| program.command("delete").description(CLI_DOCS.delete.description).argument("<bucket>", "Bucket name").argument("<key>", "File key").option("--dry-run", "Preview without deleting").addHelpText("after", renderCommandHelp("delete")).action( | ||
| async (bucket, key, options) => { | ||
| try { | ||
| const { deleteFile } = await import("./delete-YEXSMG4I.js"); | ||
| await deleteFile(bucket, key, options); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } | ||
| program.command("delete").description(CLI_DOCS.delete.description).argument("<bucket>", "Bucket name").argument("<key>", "File key").option("--dry-run", "Preview without deleting").addHelpText("after", renderCommandHelp("delete")).action(async (bucket, key, options) => { | ||
| try { | ||
| const { deleteFile } = await import("./delete-CQJEGLP3.js"); | ||
| await deleteFile(bucket, key, options); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| ); | ||
| }); | ||
| program.command("whoami").description(CLI_DOCS.whoami.description).addHelpText("after", renderCommandHelp("whoami")).action(async () => { | ||
| try { | ||
| const { whoami } = await import("./whoami-TVRKBM74.js"); | ||
| const { whoami } = await import("./whoami-WUQDFC5P.js"); | ||
| await whoami(); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -459,4 +436,4 @@ }); | ||
| await openBucket(bucket); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -466,6 +443,6 @@ }); | ||
| try { | ||
| const { describeCommand } = await import("./describe-HSEHMJVD.js"); | ||
| const { describeCommand } = await import("./describe-NH3K3LLW.js"); | ||
| describeCommand(command ?? ""); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -475,6 +452,6 @@ }); | ||
| try { | ||
| const { startMcpServer } = await import("./mcp-RZT4TJEX.js"); | ||
| const { startMcpServer } = await import("./mcp-TUZZB2C7.js"); | ||
| await startMcpServer(); | ||
| } catch (err) { | ||
| handleError(err); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
@@ -487,3 +464,8 @@ }); | ||
| if (args.length === 0 || args.length === 1 && opts.interactive) { | ||
| import("./app-Q6EW7VSM.js").then(({ startInteractive }) => startInteractive()).catch(handleError); | ||
| try { | ||
| const { startInteractive } = await import("./app-ZIHTOHXL.js"); | ||
| await startInteractive(); | ||
| } catch (error) { | ||
| handleError(error); | ||
| } | ||
| } |
+12
-12
| { | ||
| "name": "stow-cli", | ||
| "version": "2.2.1", | ||
| "type": "module", | ||
| "version": "2.2.3", | ||
| "description": "CLI for Stow file storage", | ||
| "keywords": [ | ||
| "cli", | ||
| "file-storage", | ||
| "stow", | ||
| "upload" | ||
| ], | ||
| "homepage": "https://stow.sh", | ||
| "license": "MIT", | ||
@@ -12,9 +18,2 @@ "repository": { | ||
| }, | ||
| "homepage": "https://stow.sh", | ||
| "keywords": [ | ||
| "stow", | ||
| "file-storage", | ||
| "cli", | ||
| "upload" | ||
| ], | ||
| "bin": { | ||
@@ -26,7 +25,8 @@ "stow": "./dist/cli.js" | ||
| ], | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "tsup src/cli.ts --format esm --shims", | ||
| "dev": "tsup src/cli.ts --format esm --shims --watch", | ||
| "test": "vitest run", | ||
| "test:watch": "vitest" | ||
| "test": "vp test run", | ||
| "test:watch": "vp test" | ||
| }, | ||
@@ -52,4 +52,4 @@ "dependencies": { | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^4.1.0" | ||
| "vite-plus": "catalog:" | ||
| } | ||
| } |
+10
-10
@@ -34,7 +34,7 @@ # stow-cli | ||
| | Variable | Required | Description | | ||
| |---|---|---| | ||
| | `STOW_API_KEY` | Yes | Your Stow API key (get one at `app.stow.sh/dashboard/api-keys`) | | ||
| | `STOW_API_URL` | No | Override the default API URL (`https://api.stow.sh`) | | ||
| | `STOW_ADMIN_SECRET` | No | Required for `admin` commands only | | ||
| | Variable | Required | Description | | ||
| | ------------------- | -------- | --------------------------------------------------------------- | | ||
| | `STOW_API_KEY` | Yes | Your Stow API key (get one at `app.stow.sh/dashboard/api-keys`) | | ||
| | `STOW_API_URL` | No | Override the default API URL (`https://api.stow.sh`) | | ||
| | `STOW_ADMIN_SECRET` | No | Required for `admin` commands only | | ||
@@ -356,7 +356,7 @@ ## Commands | ||
| | Variable | Default | Description | | ||
| |---|---|---| | ||
| | `STOW_API_KEY` | -- | API key for authentication | | ||
| | `STOW_API_URL` | `https://api.stow.sh` | API base URL | | ||
| | `STOW_ADMIN_SECRET` | -- | Secret for admin commands | | ||
| | Variable | Default | Description | | ||
| | ------------------- | --------------------- | -------------------------- | | ||
| | `STOW_API_KEY` | -- | API key for authentication | | ||
| | `STOW_API_URL` | `https://api.stow.sh` | API base URL | | ||
| | `STOW_ADMIN_SECRET` | -- | Secret for admin commands | | ||
@@ -363,0 +363,0 @@ ## License |
Unpublished package
Supply chain riskPackage version was not found on the registry. It may exist on a different registry and need to be configured to pull from that registry.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Unpublished package
Supply chain riskPackage version was not found on the registry. It may exist on a different registry and need to be configured to pull from that registry.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
451732
31.11%165
32%15448
33.21%22
10%25
19.05%