ossput 0.0.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/.cursor/skills/ossput/SKILL.md +87 -0
- package/.cursor/skills/ossput/examples.md +59 -0
- package/.cursor/skills/ossput/reference.md +78 -0
- package/.ossput.json.example +3 -0
- package/AGENTS.md +16 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/config.index.example.json +8 -0
- package/dist/batch-upload.d.ts +28 -0
- package/dist/batch-upload.js +34 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +253 -0
- package/dist/config-profiles.d.ts +45 -0
- package/dist/config-profiles.js +282 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +81 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/delete-object.d.ts +6 -0
- package/dist/delete-object.js +26 -0
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.js +128 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +21 -0
- package/dist/key-builder.d.ts +1 -0
- package/dist/key-builder.js +13 -0
- package/dist/list-directories.d.ts +11 -0
- package/dist/list-directories.js +64 -0
- package/dist/list-format.d.ts +6 -0
- package/dist/list-format.js +57 -0
- package/dist/list-objects.d.ts +7 -0
- package/dist/list-objects.js +47 -0
- package/dist/mcp-annotations.d.ts +16 -0
- package/dist/mcp-annotations.js +16 -0
- package/dist/mcp-result.d.ts +14 -0
- package/dist/mcp-result.js +57 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +244 -0
- package/dist/object-key.d.ts +10 -0
- package/dist/object-key.js +46 -0
- package/dist/oss-client.d.ts +4 -0
- package/dist/oss-client.js +24 -0
- package/dist/profile-cli.d.ts +16 -0
- package/dist/profile-cli.js +191 -0
- package/dist/setup/connectivity.d.ts +2 -0
- package/dist/setup/connectivity.js +5 -0
- package/dist/setup/logo.d.ts +1 -0
- package/dist/setup/logo.js +30 -0
- package/dist/setup/mcp-registry.d.ts +17 -0
- package/dist/setup/mcp-registry.js +128 -0
- package/dist/setup/prompts.d.ts +24 -0
- package/dist/setup/prompts.js +410 -0
- package/dist/setup/run-setup.d.ts +9 -0
- package/dist/setup/run-setup.js +156 -0
- package/dist/setup/skill-install.d.ts +19 -0
- package/dist/setup/skill-install.js +85 -0
- package/dist/setup/ui.d.ts +49 -0
- package/dist/setup/ui.js +164 -0
- package/dist/setup/write-config.d.ts +5 -0
- package/dist/setup/write-config.js +5 -0
- package/dist/skill-cli.d.ts +6 -0
- package/dist/skill-cli.js +34 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +1 -0
- package/dist/upload-pipeline.d.ts +7 -0
- package/dist/upload-pipeline.js +96 -0
- package/dist/validators.d.ts +10 -0
- package/dist/validators.js +77 -0
- package/docs/ram-policy.example.json +31 -0
- package/package.json +59 -0
- package/profiles.example/default.json +21 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { runSetup } from "./setup/run-setup.js";
|
|
2
|
+
import { printAsciiLogo } from "./setup/logo.js";
|
|
3
|
+
import { loadConfig, configExists, resolveConfigPath } from "./config.js";
|
|
4
|
+
import { formatDirectoriesAsMarkdown, listDirectories } from "./list-directories.js";
|
|
5
|
+
import { formatListAsMarkdown } from "./list-format.js";
|
|
6
|
+
import { listObjects } from "./list-objects.js";
|
|
7
|
+
import { uploadFile } from "./upload-pipeline.js";
|
|
8
|
+
import { batchUploadFile } from "./batch-upload.js";
|
|
9
|
+
import { deleteObject } from "./delete-object.js";
|
|
10
|
+
import { getSetupStatusJson } from "./mcp.js";
|
|
11
|
+
import { getPackageVersion } from "./setup/mcp-registry.js";
|
|
12
|
+
import { runProfileCommand } from "./profile-cli.js";
|
|
13
|
+
import { runSkillCommand } from "./skill-cli.js";
|
|
14
|
+
import { runDoctor } from "./doctor.js";
|
|
15
|
+
import { ui } from "./setup/ui.js";
|
|
16
|
+
function printUsage() {
|
|
17
|
+
console.log(`ossput — 阿里云 OSS 直传(MCP + 命令行)
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
ossput 启动 MCP 服务
|
|
21
|
+
ossput setup 安装向导(首个账号 + MCP)
|
|
22
|
+
ossput profile <子命令> 管理多账号(list / add / use …)
|
|
23
|
+
ossput skill install 安装 Agent Skill 到本机(setup 会自动执行)
|
|
24
|
+
ossput put <文件…> 上传一个或多个文件
|
|
25
|
+
ossput rm <objectKey> 删除对象(须 --confirm)
|
|
26
|
+
ossput ls [子目录] 列出对象(--markdown 图片预览)
|
|
27
|
+
ossput dirs [子目录] 列出目录(--markdown)
|
|
28
|
+
ossput status 状态
|
|
29
|
+
ossput doctor 诊断环境(Node/fetch/配置/MCP/Skill)
|
|
30
|
+
|
|
31
|
+
全局选项:
|
|
32
|
+
--profile <name> 指定账号(覆盖 .ossput.json)
|
|
33
|
+
|
|
34
|
+
Profile 示例:
|
|
35
|
+
ossput profile add client-a
|
|
36
|
+
ossput profile use site
|
|
37
|
+
ossput profile list
|
|
38
|
+
|
|
39
|
+
上传示例:
|
|
40
|
+
ossput put ./photo.png --subdir demo/2026-05
|
|
41
|
+
ossput put a.png b.png --subdir demo/
|
|
42
|
+
ossput rm blog/2026/05/uuid.png --confirm
|
|
43
|
+
ossput --profile client-a put ./x.zip
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
function parseArgs(argv) {
|
|
47
|
+
let profile;
|
|
48
|
+
const args = [];
|
|
49
|
+
for (let i = 0; i < argv.length; i++) {
|
|
50
|
+
if (argv[i] === "--profile" && argv[i + 1]) {
|
|
51
|
+
profile = argv[++i];
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
args.push(argv[i]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const command = args[0] ?? "";
|
|
58
|
+
const flags = {};
|
|
59
|
+
const rest = [];
|
|
60
|
+
for (let i = 1; i < args.length; i++) {
|
|
61
|
+
const a = args[i];
|
|
62
|
+
if (a === "--non-interactive")
|
|
63
|
+
flags.nonInteractive = true;
|
|
64
|
+
else if (a === "--skip-connectivity")
|
|
65
|
+
flags.skipConnectivity = true;
|
|
66
|
+
else if (a === "--set-default")
|
|
67
|
+
flags.setDefault = true;
|
|
68
|
+
else if (a === "--bind-project")
|
|
69
|
+
flags.bindProject = true;
|
|
70
|
+
else if (a === "--config" && args[i + 1]) {
|
|
71
|
+
flags.config = args[++i];
|
|
72
|
+
}
|
|
73
|
+
else if (a === "--profile-name" && args[i + 1]) {
|
|
74
|
+
flags.profileName = args[++i];
|
|
75
|
+
}
|
|
76
|
+
else if (a === "--label" && args[i + 1]) {
|
|
77
|
+
flags.label = args[++i];
|
|
78
|
+
}
|
|
79
|
+
else if (a === "--subdir" && args[i + 1]) {
|
|
80
|
+
flags.subdir = args[++i];
|
|
81
|
+
}
|
|
82
|
+
else if (a === "--content-type" && args[i + 1]) {
|
|
83
|
+
flags.contentType = args[++i];
|
|
84
|
+
}
|
|
85
|
+
else if (a === "--max-keys" && args[i + 1]) {
|
|
86
|
+
flags.maxKeys = args[++i];
|
|
87
|
+
}
|
|
88
|
+
else if (a === "--preview-max" && args[i + 1]) {
|
|
89
|
+
flags.previewMax = args[++i];
|
|
90
|
+
}
|
|
91
|
+
else if (a === "--markdown")
|
|
92
|
+
flags.markdown = true;
|
|
93
|
+
else if (a === "--images")
|
|
94
|
+
flags.images = true;
|
|
95
|
+
else if (a === "--skip-skill")
|
|
96
|
+
flags.skipSkill = true;
|
|
97
|
+
else if (a === "--cursor")
|
|
98
|
+
flags.cursor = true;
|
|
99
|
+
else if (a === "--claude")
|
|
100
|
+
flags.claude = true;
|
|
101
|
+
else if (a === "--confirm")
|
|
102
|
+
flags.confirm = true;
|
|
103
|
+
else if (a === "--stop-on-error")
|
|
104
|
+
flags.stopOnError = true;
|
|
105
|
+
else if (a.startsWith("--")) {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
rest.push(a);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { command, rest, flags, profile };
|
|
113
|
+
}
|
|
114
|
+
function loadOpts(profile) {
|
|
115
|
+
return profile ? { profile } : {};
|
|
116
|
+
}
|
|
117
|
+
export async function runCli(argv) {
|
|
118
|
+
const { command, rest, flags, profile } = parseArgs(argv);
|
|
119
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
120
|
+
printUsage();
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
if (command === "profile") {
|
|
124
|
+
return runProfileCommand(rest[0] ?? "list", rest.slice(1), flags);
|
|
125
|
+
}
|
|
126
|
+
if (command === "skill") {
|
|
127
|
+
return runSkillCommand(rest[0] ?? "install", rest.slice(1), flags);
|
|
128
|
+
}
|
|
129
|
+
if (command === "setup") {
|
|
130
|
+
const options = {
|
|
131
|
+
nonInteractive: Boolean(flags.nonInteractive),
|
|
132
|
+
configPath: typeof flags.config === "string" ? flags.config : undefined,
|
|
133
|
+
profileName: typeof flags.profileName === "string" ? flags.profileName : undefined,
|
|
134
|
+
skipConnectivity: Boolean(flags.skipConnectivity),
|
|
135
|
+
skipSkillInstall: Boolean(flags.skipSkill),
|
|
136
|
+
};
|
|
137
|
+
await runSetup(options);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
if (command === "doctor") {
|
|
141
|
+
const { checks, ok } = await runDoctor(profile);
|
|
142
|
+
for (const c of checks) {
|
|
143
|
+
const mark = c.ok ? ui.green("✓") : ui.red("✗");
|
|
144
|
+
console.log(`${mark} ${c.name}: ${c.detail}`);
|
|
145
|
+
}
|
|
146
|
+
console.log(ok ? `\n${ui.green("全部通过")}` : `\n${ui.red("存在问题,请按提示修复")}`);
|
|
147
|
+
return ok ? 0 : 1;
|
|
148
|
+
}
|
|
149
|
+
if (command === "status") {
|
|
150
|
+
const version = await getPackageVersion();
|
|
151
|
+
printAsciiLogo(version);
|
|
152
|
+
const status = await getSetupStatusJson(profile);
|
|
153
|
+
console.log(JSON.stringify(status, null, 2));
|
|
154
|
+
if (!(await configExists())) {
|
|
155
|
+
console.log("\nRun: ossput setup");
|
|
156
|
+
}
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
const cfgOpts = loadOpts(profile);
|
|
160
|
+
if (command === "dirs" || command === "directories") {
|
|
161
|
+
if (!(await configExists())) {
|
|
162
|
+
console.error(`Not configured. Run: ossput setup\nIndex: ${await resolveConfigPath()}`);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
const config = await loadConfig(cfgOpts);
|
|
166
|
+
const subdir = rest[0] ?? (typeof flags.subdir === "string" ? flags.subdir : undefined);
|
|
167
|
+
const result = await listDirectories(config, { subdir });
|
|
168
|
+
if (flags.markdown) {
|
|
169
|
+
console.log(formatDirectoriesAsMarkdown(result));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log(JSON.stringify(result, null, 2));
|
|
173
|
+
}
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
if (command === "ls" || command === "list") {
|
|
177
|
+
if (!(await configExists())) {
|
|
178
|
+
console.error(`Not configured. Run: ossput setup\nIndex: ${await resolveConfigPath()}`);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
const config = await loadConfig(cfgOpts);
|
|
182
|
+
const subdir = rest[0] ?? (typeof flags.subdir === "string" ? flags.subdir : undefined);
|
|
183
|
+
const maxKeys = typeof flags.maxKeys === "string" ? Number(flags.maxKeys) : undefined;
|
|
184
|
+
const previewMax = typeof flags.previewMax === "string" ? Number(flags.previewMax) : undefined;
|
|
185
|
+
const result = await listObjects(config, {
|
|
186
|
+
subdir,
|
|
187
|
+
maxKeys: Number.isFinite(maxKeys) ? maxKeys : undefined,
|
|
188
|
+
imagesOnly: Boolean(flags.images),
|
|
189
|
+
});
|
|
190
|
+
if (flags.markdown) {
|
|
191
|
+
console.log(formatListAsMarkdown(result, {
|
|
192
|
+
previewMax: Number.isFinite(previewMax) ? previewMax : undefined,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log(JSON.stringify(result, null, 2));
|
|
197
|
+
}
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
if (command === "put") {
|
|
201
|
+
if (rest.length === 0) {
|
|
202
|
+
console.error("Error: missing file path(s). Usage: ossput put <file> [file2 …]");
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
if (!(await configExists())) {
|
|
206
|
+
console.error(`Not configured. Run: ossput setup\nIndex: ${await resolveConfigPath()}`);
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
const config = await loadConfig(cfgOpts);
|
|
210
|
+
const subdir = typeof flags.subdir === "string" ? flags.subdir : undefined;
|
|
211
|
+
const contentType = typeof flags.contentType === "string" ? flags.contentType : undefined;
|
|
212
|
+
if (rest.length === 1) {
|
|
213
|
+
const result = await uploadFile(config, rest[0], subdir, contentType);
|
|
214
|
+
console.log(JSON.stringify(result, null, 2));
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
const batch = await batchUploadFile(config, rest, {
|
|
218
|
+
subdir,
|
|
219
|
+
contentType,
|
|
220
|
+
stopOnError: Boolean(flags.stopOnError),
|
|
221
|
+
});
|
|
222
|
+
console.log(JSON.stringify(batch, null, 2));
|
|
223
|
+
return batch.failed > 0 ? 1 : 0;
|
|
224
|
+
}
|
|
225
|
+
if (command === "rm" || command === "delete") {
|
|
226
|
+
const objectKey = rest[0];
|
|
227
|
+
if (!objectKey) {
|
|
228
|
+
console.error("Error: missing objectKey. Usage: ossput rm <objectKey> --confirm");
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
if (flags.confirm !== true) {
|
|
232
|
+
console.error("Refusing to delete without --confirm. List keys with: ossput ls");
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
if (!(await configExists())) {
|
|
236
|
+
console.error(`Not configured. Run: ossput setup\nIndex: ${await resolveConfigPath()}`);
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
const config = await loadConfig(cfgOpts);
|
|
240
|
+
try {
|
|
241
|
+
const result = await deleteObject(config, objectKey, { confirm: true });
|
|
242
|
+
console.log(JSON.stringify(result, null, 2));
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.error(err instanceof Error ? err.message : err);
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
console.error(`Unknown command: ${command}`);
|
|
251
|
+
printUsage();
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AppConfig, OssputIndexConfig, ProjectBinding, ResolvedProfile } from "./types.js";
|
|
2
|
+
import { DEFAULT_ALLOWED, formatPrefixDisplay } from "./config.js";
|
|
3
|
+
export declare const INDEX_CONFIG_PATH: string;
|
|
4
|
+
export declare const PROFILES_DIR: string;
|
|
5
|
+
export declare const PROJECT_CONFIG_FILENAME = ".ossput.json";
|
|
6
|
+
export declare function validateProfileName(name: string): string | true;
|
|
7
|
+
export declare function profileFilePath(name: string): string;
|
|
8
|
+
export declare function ensureProfilesDir(): Promise<void>;
|
|
9
|
+
export declare function parseIndex(raw: unknown): OssputIndexConfig;
|
|
10
|
+
export declare function loadIndex(): Promise<OssputIndexConfig | null>;
|
|
11
|
+
export declare function saveIndex(index: OssputIndexConfig): Promise<void>;
|
|
12
|
+
export declare function indexExists(): Promise<boolean>;
|
|
13
|
+
export declare function profileExists(name: string): Promise<boolean>;
|
|
14
|
+
export declare function parseProjectBinding(raw: unknown): ProjectBinding;
|
|
15
|
+
export declare function findProjectOssputJson(startDir?: string): Promise<string | null>;
|
|
16
|
+
export declare function readProjectBinding(cwd?: string): Promise<{
|
|
17
|
+
binding: ProjectBinding;
|
|
18
|
+
path: string;
|
|
19
|
+
} | null>;
|
|
20
|
+
export declare function writeProjectBinding(profileName: string, cwd?: string): Promise<string>;
|
|
21
|
+
export interface ResolveProfileOptions {
|
|
22
|
+
profile?: string;
|
|
23
|
+
cwd?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function resolveActiveProfile(options?: ResolveProfileOptions): Promise<ResolvedProfile>;
|
|
26
|
+
export declare function loadProfileFile(name: string): Promise<AppConfig>;
|
|
27
|
+
export interface LoadConfigOptions extends ResolveProfileOptions {
|
|
28
|
+
}
|
|
29
|
+
export declare function loadConfigWithProfile(options?: LoadConfigOptions): Promise<{
|
|
30
|
+
config: AppConfig;
|
|
31
|
+
resolved: ResolvedProfile;
|
|
32
|
+
}>;
|
|
33
|
+
export declare function configExists(): Promise<boolean>;
|
|
34
|
+
export declare function listProfileNames(): Promise<{
|
|
35
|
+
name: string;
|
|
36
|
+
label?: string;
|
|
37
|
+
isDefault: boolean;
|
|
38
|
+
}[]>;
|
|
39
|
+
export declare function saveProfile(name: string, config: AppConfig, options?: {
|
|
40
|
+
label?: string;
|
|
41
|
+
setDefault?: boolean;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
export declare function setDefaultProfile(name: string): Promise<void>;
|
|
44
|
+
export declare function removeProfile(name: string): Promise<void>;
|
|
45
|
+
export { DEFAULT_ALLOWED, formatPrefixDisplay };
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, access, unlink } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { CONFIG_DIR, DEFAULT_ALLOWED, formatPrefixDisplay, normalizePrefix, parseConfig, } from "./config.js";
|
|
5
|
+
export const INDEX_CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
6
|
+
export const PROFILES_DIR = join(CONFIG_DIR, "profiles");
|
|
7
|
+
export const PROJECT_CONFIG_FILENAME = ".ossput.json";
|
|
8
|
+
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
9
|
+
export function validateProfileName(name) {
|
|
10
|
+
const t = name.trim();
|
|
11
|
+
if (!t)
|
|
12
|
+
return "Profile 名称不能为空";
|
|
13
|
+
if (!PROFILE_NAME_RE.test(t)) {
|
|
14
|
+
return "仅支持小写字母、数字、连字符,且不能以连字符开头";
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
export function profileFilePath(name) {
|
|
19
|
+
return join(PROFILES_DIR, `${name}.json`);
|
|
20
|
+
}
|
|
21
|
+
export async function ensureProfilesDir() {
|
|
22
|
+
await mkdir(PROFILES_DIR, { recursive: true, mode: 0o700 });
|
|
23
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
export function parseIndex(raw) {
|
|
26
|
+
if (!raw || typeof raw !== "object") {
|
|
27
|
+
throw new Error("Invalid index config: expected JSON object");
|
|
28
|
+
}
|
|
29
|
+
const o = raw;
|
|
30
|
+
const version = Number(o.version ?? 1);
|
|
31
|
+
if (version !== 1) {
|
|
32
|
+
throw new Error(`Unsupported config version: ${version}`);
|
|
33
|
+
}
|
|
34
|
+
const defaultProfile = String(o.defaultProfile ?? "").trim();
|
|
35
|
+
const profilesRaw = o.profiles;
|
|
36
|
+
if (!defaultProfile || !profilesRaw || typeof profilesRaw !== "object") {
|
|
37
|
+
throw new Error("Invalid index: defaultProfile and profiles are required");
|
|
38
|
+
}
|
|
39
|
+
const profiles = {};
|
|
40
|
+
for (const [key, val] of Object.entries(profilesRaw)) {
|
|
41
|
+
const label = val && typeof val === "object" && "label" in val
|
|
42
|
+
? String(val.label ?? "").trim() || undefined
|
|
43
|
+
: undefined;
|
|
44
|
+
profiles[key] = label ? { label } : {};
|
|
45
|
+
}
|
|
46
|
+
if (!profiles[defaultProfile]) {
|
|
47
|
+
throw new Error(`defaultProfile "${defaultProfile}" is not in profiles`);
|
|
48
|
+
}
|
|
49
|
+
return { version: 1, defaultProfile, profiles };
|
|
50
|
+
}
|
|
51
|
+
export async function loadIndex() {
|
|
52
|
+
try {
|
|
53
|
+
await access(INDEX_CONFIG_PATH, constants.R_OK);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const raw = JSON.parse(await readFile(INDEX_CONFIG_PATH, "utf8"));
|
|
59
|
+
return parseIndex(raw);
|
|
60
|
+
}
|
|
61
|
+
export async function saveIndex(index) {
|
|
62
|
+
await ensureProfilesDir();
|
|
63
|
+
await writeFile(INDEX_CONFIG_PATH, `${JSON.stringify(index, null, 2)}\n`, { mode: 0o600 });
|
|
64
|
+
}
|
|
65
|
+
export async function indexExists() {
|
|
66
|
+
return (await loadIndex()) !== null;
|
|
67
|
+
}
|
|
68
|
+
export async function profileExists(name) {
|
|
69
|
+
try {
|
|
70
|
+
await access(profileFilePath(name), constants.R_OK);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function parseProjectBinding(raw) {
|
|
78
|
+
if (!raw || typeof raw !== "object") {
|
|
79
|
+
throw new Error("Invalid .ossput.json: expected object");
|
|
80
|
+
}
|
|
81
|
+
const o = raw;
|
|
82
|
+
const forbidden = ["accessKeyId", "accessKeySecret", "bucket", "region"];
|
|
83
|
+
for (const key of forbidden) {
|
|
84
|
+
if (key in o) {
|
|
85
|
+
throw new Error(`.ossput.json must not contain "${key}"; use profiles only`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const profile = String(o.profile ?? "").trim();
|
|
89
|
+
const valid = validateProfileName(profile);
|
|
90
|
+
if (valid !== true)
|
|
91
|
+
throw new Error(valid);
|
|
92
|
+
return { profile };
|
|
93
|
+
}
|
|
94
|
+
export async function findProjectOssputJson(startDir = process.cwd()) {
|
|
95
|
+
let dir = resolve(startDir);
|
|
96
|
+
const root = resolve("/");
|
|
97
|
+
while (true) {
|
|
98
|
+
const candidate = join(dir, PROJECT_CONFIG_FILENAME);
|
|
99
|
+
try {
|
|
100
|
+
await access(candidate, constants.R_OK);
|
|
101
|
+
return candidate;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* not in this dir */
|
|
105
|
+
}
|
|
106
|
+
if (dir === root)
|
|
107
|
+
break;
|
|
108
|
+
dir = dirname(dir);
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
export async function readProjectBinding(cwd) {
|
|
113
|
+
const path = await findProjectOssputJson(cwd);
|
|
114
|
+
if (!path)
|
|
115
|
+
return null;
|
|
116
|
+
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
117
|
+
return { binding: parseProjectBinding(raw), path };
|
|
118
|
+
}
|
|
119
|
+
export async function writeProjectBinding(profileName, cwd = process.cwd()) {
|
|
120
|
+
const valid = validateProfileName(profileName);
|
|
121
|
+
if (valid !== true)
|
|
122
|
+
throw new Error(valid);
|
|
123
|
+
const path = join(resolve(cwd), PROJECT_CONFIG_FILENAME);
|
|
124
|
+
await writeFile(path, `${JSON.stringify({ profile: profileName }, null, 2)}\n`, "utf8");
|
|
125
|
+
return path;
|
|
126
|
+
}
|
|
127
|
+
export async function resolveActiveProfile(options = {}) {
|
|
128
|
+
if (options.profile?.trim()) {
|
|
129
|
+
const name = options.profile.trim();
|
|
130
|
+
const valid = validateProfileName(name);
|
|
131
|
+
if (valid !== true)
|
|
132
|
+
throw new Error(valid);
|
|
133
|
+
return { name, source: "arg" };
|
|
134
|
+
}
|
|
135
|
+
const envProfile = process.env.OSSPUT_PROFILE?.trim();
|
|
136
|
+
if (envProfile) {
|
|
137
|
+
const name = envProfile;
|
|
138
|
+
const valid = validateProfileName(name);
|
|
139
|
+
if (valid !== true)
|
|
140
|
+
throw new Error(valid);
|
|
141
|
+
return { name, source: "env" };
|
|
142
|
+
}
|
|
143
|
+
const project = await readProjectBinding(options.cwd);
|
|
144
|
+
if (project) {
|
|
145
|
+
return {
|
|
146
|
+
name: project.binding.profile,
|
|
147
|
+
source: "project",
|
|
148
|
+
projectFile: project.path,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const index = await loadIndex();
|
|
152
|
+
if (!index) {
|
|
153
|
+
throw new Error("ossput is not configured. Run: ossput setup or ossput profile add <name>");
|
|
154
|
+
}
|
|
155
|
+
return { name: index.defaultProfile, source: "default" };
|
|
156
|
+
}
|
|
157
|
+
export async function loadProfileFile(name) {
|
|
158
|
+
const path = profileFilePath(name);
|
|
159
|
+
let raw;
|
|
160
|
+
try {
|
|
161
|
+
raw = JSON.parse(await readFile(path, "utf8"));
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
165
|
+
throw new Error(`Cannot load profile "${name}" at ${path}: ${msg}`);
|
|
166
|
+
}
|
|
167
|
+
return parseConfig(raw);
|
|
168
|
+
}
|
|
169
|
+
async function applyEnvOverrides(config) {
|
|
170
|
+
if (process.env.OSS_ACCESS_KEY_ID?.trim()) {
|
|
171
|
+
config.accessKeyId = process.env.OSS_ACCESS_KEY_ID.trim();
|
|
172
|
+
}
|
|
173
|
+
if (process.env.OSS_ACCESS_KEY_SECRET?.trim()) {
|
|
174
|
+
config.accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET.trim();
|
|
175
|
+
}
|
|
176
|
+
if (process.env.OSS_REGION?.trim())
|
|
177
|
+
config.region = process.env.OSS_REGION.trim();
|
|
178
|
+
if (process.env.OSS_BUCKET?.trim())
|
|
179
|
+
config.bucket = process.env.OSS_BUCKET.trim();
|
|
180
|
+
if (process.env.OSS_PREFIX?.trim()) {
|
|
181
|
+
config.prefix = normalizePrefix(process.env.OSS_PREFIX);
|
|
182
|
+
}
|
|
183
|
+
return config;
|
|
184
|
+
}
|
|
185
|
+
export async function loadConfigWithProfile(options = {}) {
|
|
186
|
+
const resolved = await resolveActiveProfile(options);
|
|
187
|
+
if (!(await profileExists(resolved.name))) {
|
|
188
|
+
throw new Error(`Profile "${resolved.name}" not found. Run: ossput profile list`);
|
|
189
|
+
}
|
|
190
|
+
const config = await applyEnvOverrides(await loadProfileFile(resolved.name));
|
|
191
|
+
return { config, resolved };
|
|
192
|
+
}
|
|
193
|
+
export async function configExists() {
|
|
194
|
+
const index = await loadIndex();
|
|
195
|
+
if (!index)
|
|
196
|
+
return false;
|
|
197
|
+
return profileExists(index.defaultProfile);
|
|
198
|
+
}
|
|
199
|
+
export async function listProfileNames() {
|
|
200
|
+
const index = await loadIndex();
|
|
201
|
+
if (!index)
|
|
202
|
+
return [];
|
|
203
|
+
return Object.keys(index.profiles)
|
|
204
|
+
.sort()
|
|
205
|
+
.map((name) => ({
|
|
206
|
+
name,
|
|
207
|
+
label: index.profiles[name]?.label,
|
|
208
|
+
isDefault: name === index.defaultProfile,
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
export async function saveProfile(name, config, options) {
|
|
212
|
+
const valid = validateProfileName(name);
|
|
213
|
+
if (valid !== true)
|
|
214
|
+
throw new Error(valid);
|
|
215
|
+
await ensureProfilesDir();
|
|
216
|
+
const payload = {
|
|
217
|
+
region: config.region,
|
|
218
|
+
bucket: config.bucket,
|
|
219
|
+
prefix: formatPrefixDisplay(config.prefix),
|
|
220
|
+
accessKeyId: config.accessKeyId,
|
|
221
|
+
accessKeySecret: config.accessKeySecret,
|
|
222
|
+
presignExpiresSec: config.presignExpiresSec,
|
|
223
|
+
allowedExtensions: config.allowedExtensions,
|
|
224
|
+
endpoint: config.endpoint ?? null,
|
|
225
|
+
};
|
|
226
|
+
await writeFile(profileFilePath(name), `${JSON.stringify(payload, null, 2)}\n`, {
|
|
227
|
+
mode: 0o600,
|
|
228
|
+
});
|
|
229
|
+
let index = await loadIndex();
|
|
230
|
+
if (!index) {
|
|
231
|
+
index = {
|
|
232
|
+
version: 1,
|
|
233
|
+
defaultProfile: name,
|
|
234
|
+
profiles: {},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
index.profiles[name] = options?.label ? { label: options.label } : {};
|
|
238
|
+
if (options?.setDefault === true) {
|
|
239
|
+
index.defaultProfile = name;
|
|
240
|
+
}
|
|
241
|
+
else if (options?.setDefault !== false) {
|
|
242
|
+
const count = Object.keys(index.profiles).length;
|
|
243
|
+
if (count === 1 || !index.defaultProfile || !index.profiles[index.defaultProfile]) {
|
|
244
|
+
index.defaultProfile = name;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
await saveIndex(index);
|
|
248
|
+
}
|
|
249
|
+
export async function setDefaultProfile(name) {
|
|
250
|
+
const index = await loadIndex();
|
|
251
|
+
if (!index)
|
|
252
|
+
throw new Error("No profiles configured");
|
|
253
|
+
if (!index.profiles[name]) {
|
|
254
|
+
throw new Error(`Profile "${name}" is not registered in index`);
|
|
255
|
+
}
|
|
256
|
+
if (!(await profileExists(name))) {
|
|
257
|
+
throw new Error(`Profile file for "${name}" does not exist`);
|
|
258
|
+
}
|
|
259
|
+
index.defaultProfile = name;
|
|
260
|
+
await saveIndex(index);
|
|
261
|
+
}
|
|
262
|
+
export async function removeProfile(name) {
|
|
263
|
+
const index = await loadIndex();
|
|
264
|
+
if (!index)
|
|
265
|
+
throw new Error("No profiles configured");
|
|
266
|
+
if (!index.profiles[name]) {
|
|
267
|
+
throw new Error(`Profile "${name}" does not exist`);
|
|
268
|
+
}
|
|
269
|
+
const names = Object.keys(index.profiles).filter((n) => n !== name);
|
|
270
|
+
if (names.length === 0) {
|
|
271
|
+
await unlink(INDEX_CONFIG_PATH).catch(() => { });
|
|
272
|
+
await unlink(profileFilePath(name)).catch(() => { });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (index.defaultProfile === name) {
|
|
276
|
+
throw new Error(`Cannot remove default profile "${name}". Run: ossput profile default <other>`);
|
|
277
|
+
}
|
|
278
|
+
delete index.profiles[name];
|
|
279
|
+
await saveIndex(index);
|
|
280
|
+
await unlink(profileFilePath(name)).catch(() => { });
|
|
281
|
+
}
|
|
282
|
+
export { DEFAULT_ALLOWED, formatPrefixDisplay };
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AppConfig } from "./types.js";
|
|
2
|
+
export declare const CONFIG_DIR: string;
|
|
3
|
+
export declare const DEFAULT_CONFIG_PATH: string;
|
|
4
|
+
declare const DEFAULT_ALLOWED: string[];
|
|
5
|
+
/** OSS 对象前缀;`/` 或空字符串表示 Bucket 根目录 */
|
|
6
|
+
export declare function normalizePrefix(prefix: string): string;
|
|
7
|
+
export declare function formatPrefixDisplay(prefix: string): string;
|
|
8
|
+
export declare function parseConfig(raw: unknown): AppConfig;
|
|
9
|
+
export declare function resolveConfigPath(): Promise<string>;
|
|
10
|
+
export declare function configExists(): Promise<boolean>;
|
|
11
|
+
export declare function loadConfig(options?: import("./config-profiles.js").LoadConfigOptions): Promise<AppConfig>;
|
|
12
|
+
export declare function ensureConfigDir(): Promise<void>;
|
|
13
|
+
export type { LoadConfigOptions } from "./config-profiles.js";
|
|
14
|
+
export { DEFAULT_ALLOWED };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const CONFIG_DIR = join(homedir(), ".config", "ossput");
|
|
4
|
+
export const DEFAULT_CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
5
|
+
const DEFAULT_ALLOWED = [
|
|
6
|
+
"png",
|
|
7
|
+
"jpg",
|
|
8
|
+
"jpeg",
|
|
9
|
+
"gif",
|
|
10
|
+
"webp",
|
|
11
|
+
"pdf",
|
|
12
|
+
"doc",
|
|
13
|
+
"docx",
|
|
14
|
+
"xls",
|
|
15
|
+
"xlsx",
|
|
16
|
+
"ppt",
|
|
17
|
+
"pptx",
|
|
18
|
+
"mp4",
|
|
19
|
+
"mov",
|
|
20
|
+
"zip",
|
|
21
|
+
];
|
|
22
|
+
/** OSS 对象前缀;`/` 或空字符串表示 Bucket 根目录 */
|
|
23
|
+
export function normalizePrefix(prefix) {
|
|
24
|
+
const trimmed = prefix.trim();
|
|
25
|
+
if (!trimmed || trimmed === "/")
|
|
26
|
+
return "";
|
|
27
|
+
return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
28
|
+
}
|
|
29
|
+
export function formatPrefixDisplay(prefix) {
|
|
30
|
+
return prefix || "/";
|
|
31
|
+
}
|
|
32
|
+
export function parseConfig(raw) {
|
|
33
|
+
if (!raw || typeof raw !== "object") {
|
|
34
|
+
throw new Error("Invalid config: expected JSON object");
|
|
35
|
+
}
|
|
36
|
+
const o = raw;
|
|
37
|
+
const region = String(o.region ?? "").trim();
|
|
38
|
+
const bucket = String(o.bucket ?? "").trim();
|
|
39
|
+
const prefix = normalizePrefix(String(o.prefix ?? "/"));
|
|
40
|
+
const accessKeyId = String(o.accessKeyId ?? "").trim();
|
|
41
|
+
const accessKeySecret = String(o.accessKeySecret ?? "").trim();
|
|
42
|
+
if (!region || !bucket || !accessKeyId || !accessKeySecret) {
|
|
43
|
+
throw new Error("Invalid config: region, bucket, accessKeyId, accessKeySecret are required");
|
|
44
|
+
}
|
|
45
|
+
const presignExpiresSec = Number(o.presignExpiresSec ?? 900);
|
|
46
|
+
const allowedExtensions = Array.isArray(o.allowedExtensions)
|
|
47
|
+
? o.allowedExtensions.map((e) => String(e).toLowerCase().replace(/^\./, ""))
|
|
48
|
+
: DEFAULT_ALLOWED;
|
|
49
|
+
return {
|
|
50
|
+
region,
|
|
51
|
+
bucket,
|
|
52
|
+
prefix,
|
|
53
|
+
accessKeyId,
|
|
54
|
+
accessKeySecret,
|
|
55
|
+
presignExpiresSec: Number.isFinite(presignExpiresSec) ? presignExpiresSec : 900,
|
|
56
|
+
allowedExtensions,
|
|
57
|
+
endpoint: o.endpoint != null ? String(o.endpoint) : null,
|
|
58
|
+
publicBaseUrl: o.publicBaseUrl != null && String(o.publicBaseUrl).trim()
|
|
59
|
+
? String(o.publicBaseUrl).trim().replace(/\/+$/, "")
|
|
60
|
+
: null,
|
|
61
|
+
allowDelete: o.allowDelete === true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export async function resolveConfigPath() {
|
|
65
|
+
const { INDEX_CONFIG_PATH } = await import("./config-profiles.js");
|
|
66
|
+
return INDEX_CONFIG_PATH;
|
|
67
|
+
}
|
|
68
|
+
export async function configExists() {
|
|
69
|
+
const { configExists: exists } = await import("./config-profiles.js");
|
|
70
|
+
return exists();
|
|
71
|
+
}
|
|
72
|
+
export async function loadConfig(options) {
|
|
73
|
+
const { loadConfigWithProfile } = await import("./config-profiles.js");
|
|
74
|
+
const { config } = await loadConfigWithProfile(options);
|
|
75
|
+
return config;
|
|
76
|
+
}
|
|
77
|
+
export async function ensureConfigDir() {
|
|
78
|
+
const { ensureProfilesDir } = await import("./config-profiles.js");
|
|
79
|
+
await ensureProfilesDir();
|
|
80
|
+
}
|
|
81
|
+
export { DEFAULT_ALLOWED };
|