s3-storage-cli 0.1.2 → 0.1.4
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 +43 -21
- package/SKILL.md +20 -15
- package/package.json +1 -1
- package/src/cli.ts +124 -4
- package/src/config.ts +140 -18
package/README.md
CHANGED
|
@@ -12,51 +12,73 @@ Runtime requirement:
|
|
|
12
12
|
|
|
13
13
|
- `bun` must be installed because the published CLI executes with Bun
|
|
14
14
|
|
|
15
|
+
Install without global npm setup:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx s3-storage-cli status
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Quick start:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
s3-storage setup
|
|
25
|
+
s3-storage status
|
|
26
|
+
```
|
|
27
|
+
|
|
15
28
|
Commands:
|
|
16
29
|
|
|
30
|
+
- `setup` prompts for required scoped config values and saves them for this CLI
|
|
17
31
|
- `status` verifies env, SQLite, and S3 connectivity
|
|
18
32
|
- `list` shows only objects tracked by this CLI
|
|
19
33
|
- `upload` uploads one or more files or directories
|
|
20
34
|
- `delete` removes tracked objects
|
|
21
35
|
- `share` returns a direct public URL or a signed private URL
|
|
22
36
|
|
|
23
|
-
|
|
37
|
+
Scoped config keys used by this CLI:
|
|
24
38
|
|
|
25
|
-
- `
|
|
26
|
-
- `
|
|
27
|
-
- `
|
|
28
|
-
- `
|
|
29
|
-
- `
|
|
39
|
+
- `S3_STORAGE_CLI_ENDPOINT`
|
|
40
|
+
- `S3_STORAGE_CLI_REGION`
|
|
41
|
+
- `S3_STORAGE_CLI_ACCESS_KEY_ID`
|
|
42
|
+
- `S3_STORAGE_CLI_SECRET_ACCESS_KEY`
|
|
43
|
+
- `S3_STORAGE_CLI_BUCKET`
|
|
30
44
|
|
|
31
45
|
Required for full readiness and public sharing:
|
|
32
46
|
|
|
33
|
-
- `
|
|
47
|
+
- `S3_STORAGE_CLI_PUBLIC_BASE_URL`
|
|
34
48
|
|
|
35
49
|
Optional env:
|
|
36
50
|
|
|
37
|
-
- `
|
|
38
|
-
- `
|
|
39
|
-
- `
|
|
40
|
-
- `
|
|
51
|
+
- `S3_STORAGE_CLI_DB_PATH`
|
|
52
|
+
- `S3_STORAGE_CLI_SHARE_TTL_SECONDS`
|
|
53
|
+
- `S3_STORAGE_CLI_SESSION_TOKEN`
|
|
54
|
+
- `S3_STORAGE_CLI_VIRTUAL_HOSTED_STYLE`
|
|
55
|
+
- `S3_STORAGE_CLI_ENV_PATH`
|
|
56
|
+
|
|
57
|
+
`setup` writes the required scoped config to the CLI-owned env file under `~/.s3-storage-cli/config.env` by default.
|
|
41
58
|
|
|
42
|
-
|
|
59
|
+
Examples:
|
|
43
60
|
|
|
44
61
|
```bash
|
|
45
|
-
|
|
62
|
+
s3-storage setup
|
|
63
|
+
s3-storage upload ./file.txt
|
|
64
|
+
s3-storage upload ./assets --public --prefix site
|
|
65
|
+
s3-storage list
|
|
66
|
+
s3-storage share site/assets/logo.png
|
|
67
|
+
s3-storage delete site/assets/logo.png
|
|
46
68
|
```
|
|
47
69
|
|
|
48
|
-
|
|
70
|
+
## Agent Skill
|
|
71
|
+
|
|
72
|
+
This repo is also compatible with `skills.sh` style skill installers.
|
|
73
|
+
|
|
74
|
+
List the skill from GitHub:
|
|
49
75
|
|
|
50
76
|
```bash
|
|
51
|
-
s3-storage
|
|
77
|
+
npx skills add muneebhashone/s3-storage-cli -l
|
|
52
78
|
```
|
|
53
79
|
|
|
54
|
-
|
|
80
|
+
Install the skill:
|
|
55
81
|
|
|
56
82
|
```bash
|
|
57
|
-
|
|
58
|
-
bun run index.ts upload ./assets --public --prefix site
|
|
59
|
-
bun run index.ts list
|
|
60
|
-
bun run index.ts share site/assets/logo.png
|
|
61
|
-
bun run index.ts delete site/assets/logo.png
|
|
83
|
+
npx skills add muneebhashone/s3-storage-cli --skill s3-storage-cli
|
|
62
84
|
```
|
package/SKILL.md
CHANGED
|
@@ -9,28 +9,33 @@ Use this repo's CLI when the task is uploading files to the configured S3-compat
|
|
|
9
9
|
|
|
10
10
|
## Required env
|
|
11
11
|
|
|
12
|
-
- `
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
15
|
-
- `
|
|
16
|
-
- `
|
|
12
|
+
- `S3_STORAGE_CLI_ENDPOINT`
|
|
13
|
+
- `S3_STORAGE_CLI_REGION`
|
|
14
|
+
- `S3_STORAGE_CLI_ACCESS_KEY_ID`
|
|
15
|
+
- `S3_STORAGE_CLI_SECRET_ACCESS_KEY`
|
|
16
|
+
- `S3_STORAGE_CLI_BUCKET`
|
|
17
17
|
|
|
18
|
-
`
|
|
18
|
+
`S3_STORAGE_CLI_PUBLIC_BASE_URL` is required for `status` to report fully ready and for public object sharing.
|
|
19
19
|
|
|
20
20
|
Optional env:
|
|
21
21
|
|
|
22
|
-
- `
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
25
|
-
- `
|
|
22
|
+
- `S3_STORAGE_CLI_DB_PATH`
|
|
23
|
+
- `S3_STORAGE_CLI_SHARE_TTL_SECONDS`
|
|
24
|
+
- `S3_STORAGE_CLI_SESSION_TOKEN`
|
|
25
|
+
- `S3_STORAGE_CLI_VIRTUAL_HOSTED_STYLE`
|
|
26
|
+
- `S3_STORAGE_CLI_ENV_PATH`
|
|
27
|
+
|
|
28
|
+
The preferred flow is `s3-storage setup`, which prompts for the required scoped values and writes them to the CLI-owned env file.
|
|
26
29
|
|
|
27
30
|
## Commands
|
|
28
31
|
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
- `
|
|
32
|
-
- `
|
|
33
|
-
- `
|
|
32
|
+
- Install first with `npm install -g s3-storage-cli`
|
|
33
|
+
- `s3-storage setup`
|
|
34
|
+
- `s3-storage status`
|
|
35
|
+
- `s3-storage list [prefix]`
|
|
36
|
+
- `s3-storage upload <paths...> [--public|--private] [--prefix <remote-prefix>]`
|
|
37
|
+
- `s3-storage delete <keys...>`
|
|
38
|
+
- `s3-storage share <key> [--expires <seconds>]`
|
|
34
39
|
|
|
35
40
|
Short aliases are available: `ls`, `up`, `rm`, `sh`, `st`.
|
|
36
41
|
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { dirname } from "node:path";
|
|
2
1
|
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
5
|
import { Catalog } from "./catalog";
|
|
4
6
|
import {
|
|
5
7
|
ensurePublicBaseUrl,
|
|
6
8
|
getReadyEnvKeys,
|
|
7
9
|
inspectEnv,
|
|
10
|
+
loadScopedEnv,
|
|
11
|
+
pickScopedEnv,
|
|
8
12
|
requireCoreConfig,
|
|
9
13
|
resolveCatalogPath,
|
|
14
|
+
resolveConfigEnvPath,
|
|
10
15
|
type AppConfig,
|
|
11
16
|
type EnvMap,
|
|
12
17
|
type Visibility,
|
|
18
|
+
writeScopedEnvFile,
|
|
13
19
|
} from "./config";
|
|
14
20
|
import { resolveUploadTargets } from "./files";
|
|
15
21
|
import { createDefaultIo, emitEnvCheck, emitError, emitJson, formatTimestamp, type CliIo } from "./output";
|
|
@@ -21,13 +27,20 @@ interface ParsedArgs {
|
|
|
21
27
|
positionals: string[];
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
interface PromptLike {
|
|
31
|
+
ask: (question: string, defaultValue?: string) => Promise<string>;
|
|
32
|
+
close?: () => void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
interface RunCliOptions {
|
|
25
36
|
catalogPath?: string;
|
|
26
37
|
cwd?: string;
|
|
27
38
|
env?: EnvMap;
|
|
39
|
+
envFilePath?: string;
|
|
28
40
|
homeDir?: string;
|
|
29
41
|
io?: CliIo;
|
|
30
42
|
now?: () => Date;
|
|
43
|
+
prompter?: PromptLike;
|
|
31
44
|
storageFactory?: (config: AppConfig) => StorageClient;
|
|
32
45
|
}
|
|
33
46
|
|
|
@@ -37,9 +50,22 @@ interface StatusCheckResult {
|
|
|
37
50
|
ok: boolean;
|
|
38
51
|
}
|
|
39
52
|
|
|
53
|
+
const SETUP_QUESTIONS: Array<{ key: string; label: string }> = [
|
|
54
|
+
{ key: "S3_STORAGE_CLI_ENDPOINT", label: "S3 endpoint URL" },
|
|
55
|
+
{ key: "S3_STORAGE_CLI_REGION", label: "S3 region" },
|
|
56
|
+
{ key: "S3_STORAGE_CLI_ACCESS_KEY_ID", label: "Access key ID" },
|
|
57
|
+
{ key: "S3_STORAGE_CLI_SECRET_ACCESS_KEY", label: "Secret access key" },
|
|
58
|
+
{ key: "S3_STORAGE_CLI_BUCKET", label: "Bucket name" },
|
|
59
|
+
{ key: "S3_STORAGE_CLI_PUBLIC_BASE_URL", label: "Public base URL" },
|
|
60
|
+
] as const;
|
|
61
|
+
|
|
40
62
|
export async function runCli(argv: string[], options: RunCliOptions = {}): Promise<number> {
|
|
41
63
|
const io = options.io ?? createDefaultIo();
|
|
42
|
-
const
|
|
64
|
+
const processEnv = options.env ?? process.env;
|
|
65
|
+
const env = await loadScopedEnv(processEnv, {
|
|
66
|
+
envFilePath: options.envFilePath,
|
|
67
|
+
homeDir: options.homeDir,
|
|
68
|
+
});
|
|
43
69
|
const cwd = options.cwd ?? process.cwd();
|
|
44
70
|
const now = options.now ?? (() => new Date());
|
|
45
71
|
const storageFactory = options.storageFactory ?? ((config: AppConfig) => new BunStorageClient(config));
|
|
@@ -52,15 +78,30 @@ export async function runCli(argv: string[], options: RunCliOptions = {}): Promi
|
|
|
52
78
|
}
|
|
53
79
|
|
|
54
80
|
switch (parsed.command) {
|
|
81
|
+
case "setup":
|
|
82
|
+
return await runSetup(parsed, {
|
|
83
|
+
env,
|
|
84
|
+
envFilePath: options.envFilePath,
|
|
85
|
+
homeDir: options.homeDir,
|
|
86
|
+
io,
|
|
87
|
+
prompter: options.prompter,
|
|
88
|
+
});
|
|
55
89
|
case "list":
|
|
56
90
|
case "ls":
|
|
57
|
-
return await runList(parsed, {
|
|
91
|
+
return await runList(parsed, {
|
|
92
|
+
catalogPath: options.catalogPath,
|
|
93
|
+
env,
|
|
94
|
+
envFilePath: options.envFilePath,
|
|
95
|
+
homeDir: options.homeDir,
|
|
96
|
+
io,
|
|
97
|
+
});
|
|
58
98
|
case "upload":
|
|
59
99
|
case "up":
|
|
60
100
|
return await runUpload(parsed, {
|
|
61
101
|
catalogPath: options.catalogPath,
|
|
62
102
|
cwd,
|
|
63
103
|
env,
|
|
104
|
+
envFilePath: options.envFilePath,
|
|
64
105
|
homeDir: options.homeDir,
|
|
65
106
|
io,
|
|
66
107
|
now,
|
|
@@ -71,6 +112,7 @@ export async function runCli(argv: string[], options: RunCliOptions = {}): Promi
|
|
|
71
112
|
return await runDelete(parsed, {
|
|
72
113
|
catalogPath: options.catalogPath,
|
|
73
114
|
env,
|
|
115
|
+
envFilePath: options.envFilePath,
|
|
74
116
|
homeDir: options.homeDir,
|
|
75
117
|
io,
|
|
76
118
|
now,
|
|
@@ -81,6 +123,7 @@ export async function runCli(argv: string[], options: RunCliOptions = {}): Promi
|
|
|
81
123
|
return await runShare(parsed, {
|
|
82
124
|
catalogPath: options.catalogPath,
|
|
83
125
|
env,
|
|
126
|
+
envFilePath: options.envFilePath,
|
|
84
127
|
homeDir: options.homeDir,
|
|
85
128
|
io,
|
|
86
129
|
storageFactory,
|
|
@@ -90,6 +133,7 @@ export async function runCli(argv: string[], options: RunCliOptions = {}): Promi
|
|
|
90
133
|
return await runStatus(parsed, {
|
|
91
134
|
catalogPath: options.catalogPath,
|
|
92
135
|
env,
|
|
136
|
+
envFilePath: options.envFilePath,
|
|
93
137
|
homeDir: options.homeDir,
|
|
94
138
|
io,
|
|
95
139
|
storageFactory,
|
|
@@ -103,9 +147,54 @@ export async function runCli(argv: string[], options: RunCliOptions = {}): Promi
|
|
|
103
147
|
}
|
|
104
148
|
}
|
|
105
149
|
|
|
150
|
+
async function runSetup(
|
|
151
|
+
parsed: ParsedArgs,
|
|
152
|
+
options: {
|
|
153
|
+
env: EnvMap;
|
|
154
|
+
envFilePath?: string;
|
|
155
|
+
homeDir?: string;
|
|
156
|
+
io: CliIo;
|
|
157
|
+
prompter?: PromptLike;
|
|
158
|
+
},
|
|
159
|
+
): Promise<number> {
|
|
160
|
+
if (parsed.positionals.length > 0) {
|
|
161
|
+
throw new Error("setup does not accept positional arguments");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const envFilePath = options.envFilePath ?? resolveConfigEnvPath(options.env, options.homeDir);
|
|
165
|
+
await mkdir(dirname(envFilePath), { recursive: true });
|
|
166
|
+
|
|
167
|
+
const prompter = options.prompter ?? createPrompter();
|
|
168
|
+
const nextEnv = {
|
|
169
|
+
...pickScopedEnv(options.env),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
for (const question of SETUP_QUESTIONS) {
|
|
174
|
+
const answer = await promptRequiredValue(prompter, question.label, nextEnv[question.key]);
|
|
175
|
+
nextEnv[question.key] = answer;
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
await prompter.close?.();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await writeScopedEnvFile(envFilePath, nextEnv);
|
|
182
|
+
|
|
183
|
+
if (parsed.flags.json) {
|
|
184
|
+
emitJson(options.io, {
|
|
185
|
+
envFilePath,
|
|
186
|
+
savedKeys: SETUP_QUESTIONS.map((question) => question.key),
|
|
187
|
+
});
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
options.io.stdout(`saved\t${envFilePath}`);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
106
195
|
async function runList(
|
|
107
196
|
parsed: ParsedArgs,
|
|
108
|
-
options: { catalogPath?: string; env: EnvMap; homeDir?: string; io: CliIo },
|
|
197
|
+
options: { catalogPath?: string; env: EnvMap; envFilePath?: string; homeDir?: string; io: CliIo },
|
|
109
198
|
): Promise<number> {
|
|
110
199
|
if (parsed.positionals.length > 1) {
|
|
111
200
|
throw new Error("list accepts at most one prefix");
|
|
@@ -113,6 +202,7 @@ async function runList(
|
|
|
113
202
|
|
|
114
203
|
const config = requireCoreConfig(options.env, {
|
|
115
204
|
catalogPath: options.catalogPath,
|
|
205
|
+
envFilePath: options.envFilePath,
|
|
116
206
|
homeDir: options.homeDir,
|
|
117
207
|
});
|
|
118
208
|
|
|
@@ -141,6 +231,7 @@ async function runUpload(
|
|
|
141
231
|
catalogPath?: string;
|
|
142
232
|
cwd: string;
|
|
143
233
|
env: EnvMap;
|
|
234
|
+
envFilePath?: string;
|
|
144
235
|
homeDir?: string;
|
|
145
236
|
io: CliIo;
|
|
146
237
|
now: () => Date;
|
|
@@ -151,6 +242,7 @@ async function runUpload(
|
|
|
151
242
|
const prefix = readOptionalStringFlag(parsed.flags, "prefix");
|
|
152
243
|
const config = requireCoreConfig(options.env, {
|
|
153
244
|
catalogPath: options.catalogPath,
|
|
245
|
+
envFilePath: options.envFilePath,
|
|
154
246
|
homeDir: options.homeDir,
|
|
155
247
|
});
|
|
156
248
|
|
|
@@ -208,6 +300,7 @@ async function runDelete(
|
|
|
208
300
|
options: {
|
|
209
301
|
catalogPath?: string;
|
|
210
302
|
env: EnvMap;
|
|
303
|
+
envFilePath?: string;
|
|
211
304
|
homeDir?: string;
|
|
212
305
|
io: CliIo;
|
|
213
306
|
now: () => Date;
|
|
@@ -220,6 +313,7 @@ async function runDelete(
|
|
|
220
313
|
|
|
221
314
|
const config = requireCoreConfig(options.env, {
|
|
222
315
|
catalogPath: options.catalogPath,
|
|
316
|
+
envFilePath: options.envFilePath,
|
|
223
317
|
homeDir: options.homeDir,
|
|
224
318
|
});
|
|
225
319
|
const storage = options.storageFactory(config);
|
|
@@ -271,6 +365,7 @@ async function runShare(
|
|
|
271
365
|
options: {
|
|
272
366
|
catalogPath?: string;
|
|
273
367
|
env: EnvMap;
|
|
368
|
+
envFilePath?: string;
|
|
274
369
|
homeDir?: string;
|
|
275
370
|
io: CliIo;
|
|
276
371
|
storageFactory: (config: AppConfig) => StorageClient;
|
|
@@ -282,6 +377,7 @@ async function runShare(
|
|
|
282
377
|
|
|
283
378
|
const config = requireCoreConfig(options.env, {
|
|
284
379
|
catalogPath: options.catalogPath,
|
|
380
|
+
envFilePath: options.envFilePath,
|
|
285
381
|
homeDir: options.homeDir,
|
|
286
382
|
});
|
|
287
383
|
const catalog = new Catalog(config.catalogPath);
|
|
@@ -322,6 +418,7 @@ async function runStatus(
|
|
|
322
418
|
options: {
|
|
323
419
|
catalogPath?: string;
|
|
324
420
|
env: EnvMap;
|
|
421
|
+
envFilePath?: string;
|
|
325
422
|
homeDir?: string;
|
|
326
423
|
io: CliIo;
|
|
327
424
|
storageFactory: (config: AppConfig) => StorageClient;
|
|
@@ -369,6 +466,7 @@ async function runStatus(
|
|
|
369
466
|
try {
|
|
370
467
|
config = requireCoreConfig(options.env, {
|
|
371
468
|
catalogPath: options.catalogPath,
|
|
469
|
+
envFilePath: options.envFilePath,
|
|
372
470
|
homeDir: options.homeDir,
|
|
373
471
|
});
|
|
374
472
|
} catch (error) {
|
|
@@ -505,8 +603,30 @@ function readOptionalPositiveIntegerFlag(
|
|
|
505
603
|
return parsed;
|
|
506
604
|
}
|
|
507
605
|
|
|
606
|
+
async function promptRequiredValue(prompt: PromptLike, label: string, defaultValue?: string): Promise<string> {
|
|
607
|
+
while (true) {
|
|
608
|
+
const answer = (await prompt.ask(label, defaultValue)).trim();
|
|
609
|
+
if (answer) {
|
|
610
|
+
return answer;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function createPrompter(): PromptLike {
|
|
616
|
+
const readline = createInterface({ input, output });
|
|
617
|
+
return {
|
|
618
|
+
ask: async (question: string, defaultValue?: string) => {
|
|
619
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
620
|
+
const answer = await readline.question(`${question}${suffix}: `);
|
|
621
|
+
return answer.trim() || defaultValue || "";
|
|
622
|
+
},
|
|
623
|
+
close: () => readline.close(),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
508
627
|
function printHelp(io: CliIo): void {
|
|
509
628
|
io.stdout("s3-storage-cli");
|
|
629
|
+
io.stdout("setup [--json]");
|
|
510
630
|
io.stdout("status [--json]");
|
|
511
631
|
io.stdout("list|ls [prefix] [--json]");
|
|
512
632
|
io.stdout("upload|up <paths...> [--public|--private] [--prefix <remote-prefix>] [--json]");
|
package/src/config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
|
|
@@ -10,6 +11,7 @@ export interface AppConfig {
|
|
|
10
11
|
bucket: string;
|
|
11
12
|
catalogPath: string;
|
|
12
13
|
endpoint: string;
|
|
14
|
+
envFilePath: string;
|
|
13
15
|
publicBaseUrl?: string;
|
|
14
16
|
region: string;
|
|
15
17
|
secretAccessKey: string;
|
|
@@ -26,19 +28,33 @@ export interface EnvCheck {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const CORE_ENV_KEYS = [
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
31
|
+
"S3_STORAGE_CLI_ENDPOINT",
|
|
32
|
+
"S3_STORAGE_CLI_REGION",
|
|
33
|
+
"S3_STORAGE_CLI_ACCESS_KEY_ID",
|
|
34
|
+
"S3_STORAGE_CLI_SECRET_ACCESS_KEY",
|
|
35
|
+
"S3_STORAGE_CLI_BUCKET",
|
|
34
36
|
] as const;
|
|
35
37
|
|
|
36
|
-
const READY_ENV_KEYS = [...CORE_ENV_KEYS, "
|
|
38
|
+
const READY_ENV_KEYS = [...CORE_ENV_KEYS, "S3_STORAGE_CLI_PUBLIC_BASE_URL"] as const;
|
|
39
|
+
|
|
40
|
+
const OPTIONAL_SCOPED_ENV_KEYS = [
|
|
41
|
+
"S3_STORAGE_CLI_SESSION_TOKEN",
|
|
42
|
+
"S3_STORAGE_CLI_SHARE_TTL_SECONDS",
|
|
43
|
+
"S3_STORAGE_CLI_VIRTUAL_HOSTED_STYLE",
|
|
44
|
+
"S3_STORAGE_CLI_DB_PATH",
|
|
45
|
+
"S3_STORAGE_CLI_ENV_PATH",
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
const ALL_SCOPED_ENV_KEYS = [...READY_ENV_KEYS, ...OPTIONAL_SCOPED_ENV_KEYS] as const;
|
|
37
49
|
|
|
38
50
|
export function getReadyEnvKeys(): readonly string[] {
|
|
39
51
|
return READY_ENV_KEYS;
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
export function getScopedEnvKeys(): readonly string[] {
|
|
55
|
+
return ALL_SCOPED_ENV_KEYS;
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
export function inspectEnv(env: EnvMap): EnvCheck[] {
|
|
43
59
|
return READY_ENV_KEYS.map((key) => {
|
|
44
60
|
const value = env[key];
|
|
@@ -57,7 +73,7 @@ export function getMissingCoreEnvKeys(env: EnvMap): string[] {
|
|
|
57
73
|
|
|
58
74
|
export function requireCoreConfig(
|
|
59
75
|
env: EnvMap,
|
|
60
|
-
options: { homeDir?: string; catalogPath?: string } = {},
|
|
76
|
+
options: { homeDir?: string; catalogPath?: string; envFilePath?: string } = {},
|
|
61
77
|
): AppConfig {
|
|
62
78
|
const missing = getMissingCoreEnvKeys(env);
|
|
63
79
|
if (missing.length > 0) {
|
|
@@ -65,21 +81,26 @@ export function requireCoreConfig(
|
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
return {
|
|
68
|
-
accessKeyId: env.
|
|
69
|
-
bucket: env.
|
|
84
|
+
accessKeyId: env.S3_STORAGE_CLI_ACCESS_KEY_ID!.trim(),
|
|
85
|
+
bucket: env.S3_STORAGE_CLI_BUCKET!.trim(),
|
|
70
86
|
catalogPath: options.catalogPath ?? resolveCatalogPath(env, options.homeDir),
|
|
71
|
-
endpoint: env.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
endpoint: env.S3_STORAGE_CLI_ENDPOINT!.trim(),
|
|
88
|
+
envFilePath: options.envFilePath ?? resolveConfigEnvPath(env, options.homeDir),
|
|
89
|
+
publicBaseUrl: env.S3_STORAGE_CLI_PUBLIC_BASE_URL?.trim() || undefined,
|
|
90
|
+
region: env.S3_STORAGE_CLI_REGION!.trim(),
|
|
91
|
+
secretAccessKey: env.S3_STORAGE_CLI_SECRET_ACCESS_KEY!.trim(),
|
|
92
|
+
sessionToken: env.S3_STORAGE_CLI_SESSION_TOKEN?.trim() || undefined,
|
|
93
|
+
shareTtlSeconds: parsePositiveInteger(
|
|
94
|
+
env.S3_STORAGE_CLI_SHARE_TTL_SECONDS,
|
|
95
|
+
3600,
|
|
96
|
+
"S3_STORAGE_CLI_SHARE_TTL_SECONDS",
|
|
97
|
+
),
|
|
98
|
+
virtualHostedStyle: parseBoolean(env.S3_STORAGE_CLI_VIRTUAL_HOSTED_STYLE, false),
|
|
78
99
|
};
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
export function resolveCatalogPath(env: EnvMap, customHomeDir?: string): string {
|
|
82
|
-
const override = env.
|
|
103
|
+
const override = env.S3_STORAGE_CLI_DB_PATH?.trim();
|
|
83
104
|
if (override) {
|
|
84
105
|
return override;
|
|
85
106
|
}
|
|
@@ -87,14 +108,115 @@ export function resolveCatalogPath(env: EnvMap, customHomeDir?: string): string
|
|
|
87
108
|
return join(customHomeDir ?? homedir(), ".s3-storage-cli", "catalog.sqlite");
|
|
88
109
|
}
|
|
89
110
|
|
|
111
|
+
export function resolveConfigEnvPath(env: EnvMap, customHomeDir?: string): string {
|
|
112
|
+
const override = env.S3_STORAGE_CLI_ENV_PATH?.trim();
|
|
113
|
+
if (override) {
|
|
114
|
+
return override;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return join(customHomeDir ?? homedir(), ".s3-storage-cli", "config.env");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function loadScopedEnv(
|
|
121
|
+
processEnv: EnvMap,
|
|
122
|
+
options: { envFilePath?: string; homeDir?: string } = {},
|
|
123
|
+
): Promise<EnvMap> {
|
|
124
|
+
const envFilePath = options.envFilePath ?? resolveConfigEnvPath(processEnv, options.homeDir);
|
|
125
|
+
const fileEnv = await readScopedEnvFile(envFilePath);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
...fileEnv,
|
|
129
|
+
...processEnv,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function readScopedEnvFile(envFilePath: string): Promise<EnvMap> {
|
|
134
|
+
try {
|
|
135
|
+
const content = await readFile(envFilePath, "utf8");
|
|
136
|
+
return parseEnvFile(content);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function writeScopedEnvFile(envFilePath: string, values: EnvMap): Promise<void> {
|
|
147
|
+
const lines = [
|
|
148
|
+
"# s3-storage-cli scoped configuration",
|
|
149
|
+
...ALL_SCOPED_ENV_KEYS.flatMap((key) => {
|
|
150
|
+
const value = values[key];
|
|
151
|
+
if (!value?.trim()) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return [`${key}=${JSON.stringify(value.trim())}`];
|
|
156
|
+
}),
|
|
157
|
+
"",
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
await writeFile(envFilePath, lines.join("\n"), "utf8");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function pickScopedEnv(env: EnvMap): EnvMap {
|
|
164
|
+
const result: EnvMap = {};
|
|
165
|
+
for (const key of ALL_SCOPED_ENV_KEYS) {
|
|
166
|
+
const value = env[key];
|
|
167
|
+
if (value !== undefined) {
|
|
168
|
+
result[key] = value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
90
174
|
export function ensurePublicBaseUrl(config: AppConfig): string {
|
|
91
175
|
if (!config.publicBaseUrl) {
|
|
92
|
-
throw new Error("missing env
|
|
176
|
+
throw new Error("missing env S3_STORAGE_CLI_PUBLIC_BASE_URL");
|
|
93
177
|
}
|
|
94
178
|
|
|
95
179
|
return config.publicBaseUrl;
|
|
96
180
|
}
|
|
97
181
|
|
|
182
|
+
function parseEnvFile(content: string): EnvMap {
|
|
183
|
+
const env: EnvMap = {};
|
|
184
|
+
|
|
185
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
186
|
+
const line = rawLine.trim();
|
|
187
|
+
if (!line || line.startsWith("#")) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const separatorIndex = line.indexOf("=");
|
|
192
|
+
if (separatorIndex <= 0) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
197
|
+
const rawValue = line.slice(separatorIndex + 1).trim();
|
|
198
|
+
if (!key) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
env[key] = parseEnvValue(rawValue);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return env;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseEnvValue(value: string): string {
|
|
209
|
+
if (value.startsWith("\"") && value.endsWith("\"")) {
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse(value) as string;
|
|
212
|
+
} catch {
|
|
213
|
+
return value.slice(1, -1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
|
|
98
220
|
function parsePositiveInteger(raw: string | undefined, fallback: number, key: string): number {
|
|
99
221
|
if (!raw?.trim()) {
|
|
100
222
|
return fallback;
|