@seanmozeik/s3up 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -0
- package/package.json +55 -0
- package/src/commands/list.test.ts +31 -0
- package/src/commands/list.ts +58 -0
- package/src/commands/prune.test.ts +83 -0
- package/src/commands/prune.ts +159 -0
- package/src/commands/upload.ts +350 -0
- package/src/index.test.ts +51 -0
- package/src/index.ts +165 -0
- package/src/lib/archive.test.ts +65 -0
- package/src/lib/archive.ts +89 -0
- package/src/lib/clipboard.ts +41 -0
- package/src/lib/flags.test.ts +72 -0
- package/src/lib/flags.ts +129 -0
- package/src/lib/multipart.ts +507 -0
- package/src/lib/output.test.ts +74 -0
- package/src/lib/output.ts +63 -0
- package/src/lib/progress-bar.ts +155 -0
- package/src/lib/providers.ts +150 -0
- package/src/lib/s3.test.ts +42 -0
- package/src/lib/s3.ts +124 -0
- package/src/lib/secrets.ts +81 -0
- package/src/lib/signing.ts +151 -0
- package/src/lib/state.ts +145 -0
- package/src/lib/upload.ts +120 -0
- package/src/ui/banner.ts +47 -0
- package/src/ui/setup.ts +161 -0
- package/src/ui/theme.ts +140 -0
package/src/lib/state.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/lib/state.ts
|
|
2
|
+
// Manages .s3up state files for resumable uploads
|
|
3
|
+
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export interface CompletedPart {
|
|
7
|
+
partNumber: number;
|
|
8
|
+
etag: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UploadState {
|
|
12
|
+
version: 1;
|
|
13
|
+
uploadId: string;
|
|
14
|
+
bucket: string;
|
|
15
|
+
key: string;
|
|
16
|
+
fileSize: number;
|
|
17
|
+
fileModified: number;
|
|
18
|
+
chunkSize: number;
|
|
19
|
+
totalParts: number;
|
|
20
|
+
completedParts: CompletedPart[];
|
|
21
|
+
createdAt: number;
|
|
22
|
+
provider: string;
|
|
23
|
+
endpoint: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the state file path for a given file
|
|
28
|
+
*/
|
|
29
|
+
export function getStateFilePath(filePath: string): string {
|
|
30
|
+
const dir = path.dirname(filePath);
|
|
31
|
+
const name = path.basename(filePath);
|
|
32
|
+
return path.join(dir, `.${name}.s3up`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load state from file, returns null if not found or invalid
|
|
37
|
+
*/
|
|
38
|
+
export async function loadState(filePath: string): Promise<UploadState | null> {
|
|
39
|
+
const stateFile = getStateFilePath(filePath);
|
|
40
|
+
const file = Bun.file(stateFile);
|
|
41
|
+
|
|
42
|
+
if (!(await file.exists())) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const content = await file.text();
|
|
48
|
+
const state = JSON.parse(content) as UploadState;
|
|
49
|
+
|
|
50
|
+
// Validate version
|
|
51
|
+
if (state.version !== 1) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return state;
|
|
56
|
+
} catch {
|
|
57
|
+
// Corrupted state file
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Save state to file
|
|
64
|
+
*/
|
|
65
|
+
export async function saveState(filePath: string, state: UploadState): Promise<void> {
|
|
66
|
+
const stateFile = getStateFilePath(filePath);
|
|
67
|
+
await Bun.write(stateFile, JSON.stringify(state, null, 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Delete state file
|
|
72
|
+
*/
|
|
73
|
+
export async function deleteState(filePath: string): Promise<void> {
|
|
74
|
+
const stateFile = getStateFilePath(filePath);
|
|
75
|
+
const file = Bun.file(stateFile);
|
|
76
|
+
|
|
77
|
+
if (await file.exists()) {
|
|
78
|
+
await Bun.write(stateFile, ''); // Clear content
|
|
79
|
+
const fs = await import('node:fs/promises');
|
|
80
|
+
await fs.unlink(stateFile);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Add a completed part to state and save
|
|
86
|
+
*/
|
|
87
|
+
export async function addCompletedPart(
|
|
88
|
+
filePath: string,
|
|
89
|
+
state: UploadState,
|
|
90
|
+
part: CompletedPart
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
// Check if part already exists
|
|
93
|
+
const existing = state.completedParts.findIndex((p) => p.partNumber === part.partNumber);
|
|
94
|
+
if (existing >= 0) {
|
|
95
|
+
state.completedParts[existing] = part;
|
|
96
|
+
} else {
|
|
97
|
+
state.completedParts.push(part);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Keep sorted by part number
|
|
101
|
+
state.completedParts.sort((a, b) => a.partNumber - b.partNumber);
|
|
102
|
+
|
|
103
|
+
await saveState(filePath, state);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if file has changed since state was created
|
|
108
|
+
*/
|
|
109
|
+
export async function hasFileChanged(filePath: string, state: UploadState): Promise<boolean> {
|
|
110
|
+
const file = Bun.file(filePath);
|
|
111
|
+
const stat = await file.stat();
|
|
112
|
+
|
|
113
|
+
return file.size !== state.fileSize || stat.mtime.getTime() !== state.fileModified;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create initial state for a new upload
|
|
118
|
+
*/
|
|
119
|
+
export function createInitialState(
|
|
120
|
+
uploadId: string,
|
|
121
|
+
bucket: string,
|
|
122
|
+
key: string,
|
|
123
|
+
fileSize: number,
|
|
124
|
+
fileModified: number,
|
|
125
|
+
chunkSize: number,
|
|
126
|
+
provider: string,
|
|
127
|
+
endpoint: string
|
|
128
|
+
): UploadState {
|
|
129
|
+
const totalParts = Math.ceil(fileSize / chunkSize);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
bucket,
|
|
133
|
+
chunkSize,
|
|
134
|
+
completedParts: [],
|
|
135
|
+
createdAt: Date.now(),
|
|
136
|
+
endpoint,
|
|
137
|
+
fileModified,
|
|
138
|
+
fileSize,
|
|
139
|
+
key,
|
|
140
|
+
provider,
|
|
141
|
+
totalParts,
|
|
142
|
+
uploadId,
|
|
143
|
+
version: 1
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/lib/upload.ts
|
|
2
|
+
import type { S3Config } from './providers.js';
|
|
3
|
+
import { getEndpoint } from './providers.js';
|
|
4
|
+
|
|
5
|
+
export interface UploadResult {
|
|
6
|
+
filename: string;
|
|
7
|
+
size: number;
|
|
8
|
+
publicUrl: string;
|
|
9
|
+
success: true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UploadError {
|
|
13
|
+
filename: string;
|
|
14
|
+
error: string;
|
|
15
|
+
success: false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type UploadOutcome = UploadResult | UploadError;
|
|
19
|
+
|
|
20
|
+
export interface UploadProgress {
|
|
21
|
+
activeFiles: Set<string>;
|
|
22
|
+
completed: number;
|
|
23
|
+
total: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format bytes to human readable string
|
|
28
|
+
*/
|
|
29
|
+
export function formatBytes(bytes: number): string {
|
|
30
|
+
if (bytes === 0) return '0 B';
|
|
31
|
+
const k = 1024;
|
|
32
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
33
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
34
|
+
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render progress string for spinner
|
|
39
|
+
*/
|
|
40
|
+
export function renderProgress(progress: UploadProgress): string {
|
|
41
|
+
if (progress.activeFiles.size === 0) {
|
|
42
|
+
return `Uploaded ${progress.completed}/${progress.total} files`;
|
|
43
|
+
}
|
|
44
|
+
const active = [...progress.activeFiles].join(', ');
|
|
45
|
+
return `[${progress.completed}/${progress.total}] ${active}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Concurrency-limited parallel execution
|
|
50
|
+
*/
|
|
51
|
+
export async function runWithConcurrency<T, R>(
|
|
52
|
+
items: T[],
|
|
53
|
+
limit: number,
|
|
54
|
+
fn: (item: T, index: number) => Promise<R>
|
|
55
|
+
): Promise<R[]> {
|
|
56
|
+
const results: R[] = new Array(items.length);
|
|
57
|
+
const executing = new Set<Promise<void>>();
|
|
58
|
+
let currentIndex = 0;
|
|
59
|
+
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
const index = currentIndex++;
|
|
62
|
+
const promise = fn(item, index).then((result) => {
|
|
63
|
+
results[index] = result;
|
|
64
|
+
executing.delete(promise);
|
|
65
|
+
});
|
|
66
|
+
executing.add(promise);
|
|
67
|
+
|
|
68
|
+
if (executing.size >= limit) {
|
|
69
|
+
await Promise.race(executing);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await Promise.all(executing);
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Upload a single file to S3
|
|
79
|
+
*/
|
|
80
|
+
export async function uploadFile(
|
|
81
|
+
file: { path: string; name: string; size: number },
|
|
82
|
+
config: S3Config
|
|
83
|
+
): Promise<UploadOutcome> {
|
|
84
|
+
try {
|
|
85
|
+
const fileContent = Bun.file(file.path);
|
|
86
|
+
const endpoint = getEndpoint(config);
|
|
87
|
+
|
|
88
|
+
const response = await fetch(`s3://${config.bucket}/${file.name}`, {
|
|
89
|
+
body: fileContent.stream(),
|
|
90
|
+
headers: {
|
|
91
|
+
'Content-Disposition': 'attachment'
|
|
92
|
+
},
|
|
93
|
+
method: 'PUT',
|
|
94
|
+
s3: {
|
|
95
|
+
accessKeyId: config.accessKeyId,
|
|
96
|
+
endpoint,
|
|
97
|
+
secretAccessKey: config.secretAccessKey
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const publicUrl = `${config.publicUrlBase}/${file.name}`;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
filename: file.name,
|
|
109
|
+
publicUrl,
|
|
110
|
+
size: file.size,
|
|
111
|
+
success: true
|
|
112
|
+
};
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return {
|
|
115
|
+
error: err instanceof Error ? err.message : String(err),
|
|
116
|
+
filename: file.name,
|
|
117
|
+
success: false
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/ui/banner.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/ui/banner.ts
|
|
2
|
+
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import figlet from 'figlet';
|
|
5
|
+
import gradient from 'gradient-string';
|
|
6
|
+
// Embed font file for Bun standalone executable
|
|
7
|
+
// @ts-expect-error - Bun-specific import attribute
|
|
8
|
+
import fontPath from '../../node_modules/figlet/fonts/Slant.flf' with { type: 'file' };
|
|
9
|
+
import { gradientColors } from './theme.js';
|
|
10
|
+
|
|
11
|
+
// Create custom gradient using Catppuccin Frappe colors
|
|
12
|
+
const bannerGradient = gradient([...gradientColors.banner]);
|
|
13
|
+
|
|
14
|
+
// Lazy font loading for bytecode caching compatibility (no top-level await)
|
|
15
|
+
let fontLoaded = false;
|
|
16
|
+
|
|
17
|
+
async function ensureFontLoaded(): Promise<void> {
|
|
18
|
+
if (fontLoaded) return;
|
|
19
|
+
// In dev: fontPath is absolute. In bundled builds: fontPath is relative
|
|
20
|
+
// Use Bun.main for runtime path (import.meta.dir returns source dir with bytecode)
|
|
21
|
+
const resolvedFontPath = fontPath.startsWith('/') ? fontPath : join(dirname(Bun.main), fontPath);
|
|
22
|
+
const fontContent = await Bun.file(resolvedFontPath).text();
|
|
23
|
+
figlet.parseFont('Slant', fontContent);
|
|
24
|
+
fontLoaded = true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Display the ASCII art banner with gradient colors
|
|
29
|
+
*/
|
|
30
|
+
export async function showBanner(): Promise<void> {
|
|
31
|
+
await ensureFontLoaded();
|
|
32
|
+
|
|
33
|
+
const banner = figlet.textSync('s3up', {
|
|
34
|
+
font: 'Slant',
|
|
35
|
+
horizontalLayout: 'default'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const indent = ' ';
|
|
39
|
+
const indentedBanner = banner
|
|
40
|
+
.split('\n')
|
|
41
|
+
.map((line) => indent + line)
|
|
42
|
+
.join('\n');
|
|
43
|
+
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(`\n${bannerGradient(indentedBanner)}\n`);
|
|
46
|
+
console.log();
|
|
47
|
+
}
|
package/src/ui/setup.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// src/ui/setup.ts
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
|
+
import { PROVIDERS, type Provider, type S3Config, saveConfig } from '../lib/providers.js';
|
|
4
|
+
import { showBanner } from './banner.js';
|
|
5
|
+
import { frappe, theme } from './theme.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Interactive setup flow for configuring S3 credentials
|
|
9
|
+
*/
|
|
10
|
+
export async function setup(): Promise<void> {
|
|
11
|
+
await showBanner();
|
|
12
|
+
p.intro(frappe.text('Configure S3 credentials'));
|
|
13
|
+
|
|
14
|
+
// Provider selection
|
|
15
|
+
const providerOptions = Object.entries(PROVIDERS).map(([key, info]) => ({
|
|
16
|
+
hint: info.description,
|
|
17
|
+
label: info.name,
|
|
18
|
+
value: key as Provider
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const provider = await p.select({
|
|
22
|
+
message: 'Select your S3 provider:',
|
|
23
|
+
options: providerOptions
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (p.isCancel(provider)) {
|
|
27
|
+
p.outro(frappe.subtext1('Cancelled'));
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const providerInfo = PROVIDERS[provider];
|
|
32
|
+
const config: Partial<S3Config> = { provider };
|
|
33
|
+
|
|
34
|
+
// Provider-specific prompts
|
|
35
|
+
if (providerInfo.requiresRegion && providerInfo.regions) {
|
|
36
|
+
const region = await p.select({
|
|
37
|
+
message: `${providerInfo.name} Region:`,
|
|
38
|
+
options: providerInfo.regions.map((r) => ({ label: r, value: r }))
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (p.isCancel(region)) {
|
|
42
|
+
p.outro(frappe.subtext1('Cancelled'));
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
config.region = region;
|
|
46
|
+
} else if (providerInfo.requiresRegion) {
|
|
47
|
+
const region = await p.text({
|
|
48
|
+
message: `${providerInfo.name} Region:`,
|
|
49
|
+
validate: (v) => (v?.trim() ? undefined : 'Region is required')
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (p.isCancel(region)) {
|
|
53
|
+
p.outro(frappe.subtext1('Cancelled'));
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
config.region = region.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (providerInfo.requiresAccountId) {
|
|
60
|
+
const accountId = await p.text({
|
|
61
|
+
message: 'Account ID:',
|
|
62
|
+
validate: (v) => (v?.trim() ? undefined : 'Account ID is required')
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (p.isCancel(accountId)) {
|
|
66
|
+
p.outro(frappe.subtext1('Cancelled'));
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
config.accountId = accountId.trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (providerInfo.requiresEndpoint) {
|
|
73
|
+
const endpoint = await p.text({
|
|
74
|
+
message: 'S3 Endpoint URL:',
|
|
75
|
+
validate: (v) => {
|
|
76
|
+
if (!v?.trim()) return 'Endpoint is required';
|
|
77
|
+
try {
|
|
78
|
+
new URL(v.trim());
|
|
79
|
+
return undefined;
|
|
80
|
+
} catch {
|
|
81
|
+
return 'Must be a valid URL';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (p.isCancel(endpoint)) {
|
|
87
|
+
p.outro(frappe.subtext1('Cancelled'));
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
config.endpoint = endpoint.trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Common prompts
|
|
94
|
+
const accessKeyId = await p.text({
|
|
95
|
+
message: 'Access Key ID:',
|
|
96
|
+
validate: (v) => (v?.trim() ? undefined : 'Access Key ID is required')
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (p.isCancel(accessKeyId)) {
|
|
100
|
+
p.outro(frappe.subtext1('Cancelled'));
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
config.accessKeyId = accessKeyId.trim();
|
|
104
|
+
|
|
105
|
+
const secretAccessKey = await p.password({
|
|
106
|
+
message: 'Secret Access Key:',
|
|
107
|
+
validate: (v) => (v?.trim() ? undefined : 'Secret Access Key is required')
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (p.isCancel(secretAccessKey)) {
|
|
111
|
+
p.outro(frappe.subtext1('Cancelled'));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
config.secretAccessKey = secretAccessKey.trim();
|
|
115
|
+
|
|
116
|
+
const bucket = await p.text({
|
|
117
|
+
message: 'Bucket name:',
|
|
118
|
+
validate: (v) => (v?.trim() ? undefined : 'Bucket name is required')
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (p.isCancel(bucket)) {
|
|
122
|
+
p.outro(frappe.subtext1('Cancelled'));
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
config.bucket = bucket.trim();
|
|
126
|
+
|
|
127
|
+
const publicUrlBase = await p.text({
|
|
128
|
+
message: 'Public URL base (for generating links):',
|
|
129
|
+
validate: (v) => {
|
|
130
|
+
if (!v?.trim()) return 'Public URL base is required';
|
|
131
|
+
try {
|
|
132
|
+
new URL(v.trim());
|
|
133
|
+
return undefined;
|
|
134
|
+
} catch {
|
|
135
|
+
return 'Must be a valid URL';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (p.isCancel(publicUrlBase)) {
|
|
141
|
+
p.outro(frappe.subtext1('Cancelled'));
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
config.publicUrlBase = publicUrlBase.trim().replace(/\/$/, '');
|
|
145
|
+
|
|
146
|
+
// Store secrets
|
|
147
|
+
const s = p.spinner();
|
|
148
|
+
s.start('Storing credentials...');
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await saveConfig(config as S3Config);
|
|
152
|
+
s.stop(theme.success('Credentials stored securely'));
|
|
153
|
+
p.outro(
|
|
154
|
+
theme.success(`Setup complete! Run s3up <files...> to upload to ${providerInfo.name}.`)
|
|
155
|
+
);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
s.stop(theme.error('Failed to store credentials'));
|
|
158
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/ui/theme.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// src/ui/theme.ts
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
|
|
4
|
+
// Catppuccin Frappe palette
|
|
5
|
+
// https://github.com/catppuccin/catppuccin
|
|
6
|
+
const palette = {
|
|
7
|
+
base: '#303446',
|
|
8
|
+
blue: '#8caaee',
|
|
9
|
+
crust: '#232634',
|
|
10
|
+
flamingo: '#eebebe',
|
|
11
|
+
green: '#a6d189',
|
|
12
|
+
lavender: '#babbf1',
|
|
13
|
+
mantle: '#292c3c',
|
|
14
|
+
maroon: '#ea999c',
|
|
15
|
+
mauve: '#ca9ee6',
|
|
16
|
+
overlay0: '#737994',
|
|
17
|
+
overlay1: '#838ba7',
|
|
18
|
+
overlay2: '#949cbb',
|
|
19
|
+
peach: '#ef9f76',
|
|
20
|
+
pink: '#f4b8e4',
|
|
21
|
+
red: '#e78284',
|
|
22
|
+
rosewater: '#f2d5cf',
|
|
23
|
+
sapphire: '#85c1dc',
|
|
24
|
+
sky: '#99d1db',
|
|
25
|
+
subtext0: '#a5adce',
|
|
26
|
+
subtext1: '#b5bfe2',
|
|
27
|
+
surface0: '#414559',
|
|
28
|
+
surface1: '#51576d',
|
|
29
|
+
surface2: '#626880',
|
|
30
|
+
teal: '#81c8be',
|
|
31
|
+
text: '#c6d0f5',
|
|
32
|
+
yellow: '#e5c890'
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
// ANSI 256-color approximations for Catppuccin Frappe
|
|
36
|
+
const ansi = {
|
|
37
|
+
base: 236,
|
|
38
|
+
blue: 111,
|
|
39
|
+
crust: 234,
|
|
40
|
+
flamingo: 217,
|
|
41
|
+
green: 150,
|
|
42
|
+
lavender: 147,
|
|
43
|
+
mantle: 235,
|
|
44
|
+
maroon: 217,
|
|
45
|
+
mauve: 183,
|
|
46
|
+
overlay0: 60,
|
|
47
|
+
overlay1: 103,
|
|
48
|
+
overlay2: 103,
|
|
49
|
+
peach: 216,
|
|
50
|
+
pink: 218,
|
|
51
|
+
red: 210,
|
|
52
|
+
rosewater: 224,
|
|
53
|
+
sapphire: 110,
|
|
54
|
+
sky: 117,
|
|
55
|
+
subtext0: 146,
|
|
56
|
+
subtext1: 146,
|
|
57
|
+
surface0: 59,
|
|
58
|
+
surface1: 59,
|
|
59
|
+
surface2: 60,
|
|
60
|
+
teal: 116,
|
|
61
|
+
text: 189,
|
|
62
|
+
yellow: 223
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
// Color functions using ANSI 256 colors
|
|
66
|
+
function ansiColor(code: number): (text: string) => string {
|
|
67
|
+
return (text: string) => `\x1b[38;5;${code}m${text}\x1b[0m`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function ansiBg(code: number): (text: string) => string {
|
|
71
|
+
return (text: string) => `\x1b[48;5;${code}m${text}\x1b[0m`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Theme colors as functions
|
|
75
|
+
export const frappe = {
|
|
76
|
+
base: ansiColor(ansi.base),
|
|
77
|
+
bg: {
|
|
78
|
+
base: ansiBg(ansi.base),
|
|
79
|
+
surface0: ansiBg(ansi.surface0),
|
|
80
|
+
surface1: ansiBg(ansi.surface1)
|
|
81
|
+
},
|
|
82
|
+
blue: ansiColor(ansi.blue),
|
|
83
|
+
crust: ansiColor(ansi.crust),
|
|
84
|
+
flamingo: ansiColor(ansi.flamingo),
|
|
85
|
+
green: ansiColor(ansi.green),
|
|
86
|
+
lavender: ansiColor(ansi.lavender),
|
|
87
|
+
mantle: ansiColor(ansi.mantle),
|
|
88
|
+
maroon: ansiColor(ansi.maroon),
|
|
89
|
+
mauve: ansiColor(ansi.mauve),
|
|
90
|
+
overlay0: ansiColor(ansi.overlay0),
|
|
91
|
+
overlay1: ansiColor(ansi.overlay1),
|
|
92
|
+
overlay2: ansiColor(ansi.overlay2),
|
|
93
|
+
peach: ansiColor(ansi.peach),
|
|
94
|
+
pink: ansiColor(ansi.pink),
|
|
95
|
+
red: ansiColor(ansi.red),
|
|
96
|
+
rosewater: ansiColor(ansi.rosewater),
|
|
97
|
+
sapphire: ansiColor(ansi.sapphire),
|
|
98
|
+
sky: ansiColor(ansi.sky),
|
|
99
|
+
subtext0: ansiColor(ansi.subtext0),
|
|
100
|
+
subtext1: ansiColor(ansi.subtext1),
|
|
101
|
+
surface0: ansiColor(ansi.surface0),
|
|
102
|
+
surface1: ansiColor(ansi.surface1),
|
|
103
|
+
surface2: ansiColor(ansi.surface2),
|
|
104
|
+
teal: ansiColor(ansi.teal),
|
|
105
|
+
text: ansiColor(ansi.text),
|
|
106
|
+
yellow: ansiColor(ansi.yellow)
|
|
107
|
+
} as const;
|
|
108
|
+
|
|
109
|
+
// Semantic aliases for common use cases
|
|
110
|
+
export const theme = {
|
|
111
|
+
accent: frappe.mauve,
|
|
112
|
+
dim: frappe.surface2,
|
|
113
|
+
error: frappe.red,
|
|
114
|
+
info: frappe.blue,
|
|
115
|
+
link: frappe.peach,
|
|
116
|
+
muted: frappe.overlay1,
|
|
117
|
+
primary: frappe.mauve,
|
|
118
|
+
secondary: frappe.pink,
|
|
119
|
+
subtle: frappe.subtext0,
|
|
120
|
+
success: frappe.green,
|
|
121
|
+
text: frappe.text,
|
|
122
|
+
warning: frappe.yellow
|
|
123
|
+
} as const;
|
|
124
|
+
|
|
125
|
+
// Gradient colors for banner (hex values for gradient-string)
|
|
126
|
+
export const gradientColors = {
|
|
127
|
+
banner: [palette.mauve, palette.pink, palette.flamingo]
|
|
128
|
+
} as const;
|
|
129
|
+
|
|
130
|
+
// Box border colors (hex for boxen)
|
|
131
|
+
export const boxColors = {
|
|
132
|
+
default: palette.surface2,
|
|
133
|
+
error: palette.red,
|
|
134
|
+
info: palette.blue,
|
|
135
|
+
primary: palette.mauve,
|
|
136
|
+
success: palette.green
|
|
137
|
+
} as const;
|
|
138
|
+
|
|
139
|
+
// Re-export picocolors for basic formatting
|
|
140
|
+
export { pc };
|