Files
paperclip/cli/src/prompts/storage.ts
Forgotten fdd2ea6157 feat: add storage system with local disk and S3 providers
Introduces a provider-agnostic storage subsystem for file attachments.
Includes local disk and S3 backends, asset/attachment DB schemas, issue
attachment CRUD routes with multer upload, CLI configure/doctor/env
integration, and enriched issue ancestors with project/goal resolution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 10:31:56 -06:00

147 lines
3.5 KiB
TypeScript

import * as p from "@clack/prompts";
import type { StorageConfig } from "../config/schema.js";
import { resolveDefaultStorageDir, resolvePaperclipInstanceId } from "../config/home.js";
function defaultStorageBaseDir(): string {
return resolveDefaultStorageDir(resolvePaperclipInstanceId());
}
export function defaultStorageConfig(): StorageConfig {
return {
provider: "local_disk",
localDisk: {
baseDir: defaultStorageBaseDir(),
},
s3: {
bucket: "paperclip",
region: "us-east-1",
endpoint: undefined,
prefix: "",
forcePathStyle: false,
},
};
}
export async function promptStorage(current?: StorageConfig): Promise<StorageConfig> {
const base = current ?? defaultStorageConfig();
const provider = await p.select({
message: "Storage provider",
options: [
{
value: "local_disk" as const,
label: "Local disk (recommended)",
hint: "best for single-user local deployments",
},
{
value: "s3" as const,
label: "S3 compatible",
hint: "for cloud/object storage backends",
},
],
initialValue: base.provider,
});
if (p.isCancel(provider)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
if (provider === "local_disk") {
const baseDir = await p.text({
message: "Local storage base directory",
defaultValue: base.localDisk.baseDir || defaultStorageBaseDir(),
placeholder: defaultStorageBaseDir(),
validate: (value) => {
if (!value || value.trim().length === 0) return "Storage base directory is required";
},
});
if (p.isCancel(baseDir)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
return {
provider: "local_disk",
localDisk: {
baseDir: baseDir.trim(),
},
s3: base.s3,
};
}
const bucket = await p.text({
message: "S3 bucket",
defaultValue: base.s3.bucket || "paperclip",
placeholder: "paperclip",
validate: (value) => {
if (!value || value.trim().length === 0) return "Bucket is required";
},
});
if (p.isCancel(bucket)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
const region = await p.text({
message: "S3 region",
defaultValue: base.s3.region || "us-east-1",
placeholder: "us-east-1",
validate: (value) => {
if (!value || value.trim().length === 0) return "Region is required";
},
});
if (p.isCancel(region)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
const endpoint = await p.text({
message: "S3 endpoint (optional for compatible backends)",
defaultValue: base.s3.endpoint ?? "",
placeholder: "https://s3.amazonaws.com",
});
if (p.isCancel(endpoint)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
const prefix = await p.text({
message: "Object key prefix (optional)",
defaultValue: base.s3.prefix ?? "",
placeholder: "paperclip/",
});
if (p.isCancel(prefix)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
const forcePathStyle = await p.confirm({
message: "Use S3 path-style URLs?",
initialValue: base.s3.forcePathStyle ?? false,
});
if (p.isCancel(forcePathStyle)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
return {
provider: "s3",
localDisk: base.localDisk,
s3: {
bucket: bucket.trim(),
region: region.trim(),
endpoint: endpoint.trim() || undefined,
prefix: prefix.trim(),
forcePathStyle,
},
};
}