@vforsh/yadisk-cli 1.1.0

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/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@vforsh/yadisk-cli",
3
+ "version": "1.1.0",
4
+ "description": "CLI tool for Yandex.Disk file management",
5
+ "type": "module",
6
+ "bin": {
7
+ "yadisk": "./src/cli.ts"
8
+ },
9
+ "dependencies": {
10
+ "@vforsh/yadisk": "^1.1.0",
11
+ "chalk": "^5",
12
+ "commander": "^14",
13
+ "ora": "^9"
14
+ },
15
+ "devDependencies": {
16
+ "@types/bun": "latest",
17
+ "typescript": "^5.7"
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@vforsh/yadisk-cli",
3
+ "version": "1.1.0",
4
+ "description": "CLI tool for Yandex.Disk file management",
5
+ "type": "module",
6
+ "bin": {
7
+ "yadisk": "./src/cli.ts"
8
+ },
9
+ "dependencies": {
10
+ "@vforsh/yadisk": "workspace:*",
11
+ "chalk": "^5",
12
+ "commander": "^14",
13
+ "ora": "^9"
14
+ },
15
+ "devDependencies": {
16
+ "@types/bun": "latest",
17
+ "typescript": "^5.7"
18
+ }
19
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,411 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Command } from "commander"
4
+ import chalk from "chalk"
5
+ import ora from "ora"
6
+ import {
7
+ YaDiskClient,
8
+ getCredentials,
9
+ getConfig,
10
+ getConfigValue,
11
+ setConfigValue,
12
+ deleteConfigValue,
13
+ isValidConfigKey,
14
+ } from "@vforsh/yadisk"
15
+ import type { Credentials, Resource } from "@vforsh/yadisk"
16
+ import {
17
+ formatDiskInfo,
18
+ formatResourceList,
19
+ formatResource,
20
+ formatJson,
21
+ formatSize,
22
+ } from "./format"
23
+ import { basename } from "path"
24
+ import { createInterface } from "readline"
25
+
26
+ const program = new Command()
27
+
28
+ function getClient(): YaDiskClient {
29
+ const opts = program.opts()
30
+ const credentials = getCredentials({ username: opts.username, password: opts.password })
31
+ return new YaDiskClient(credentials)
32
+ }
33
+
34
+ function resolveUploadDest(file: string, dest?: string): string {
35
+ if (dest) return dest
36
+ const uploadDir = getConfigValue("upload_dir")
37
+ if (!uploadDir) {
38
+ console.error(
39
+ "Error: No destination specified.\n" +
40
+ "Provide <dest> argument or set default: yadisk config set upload_dir /path"
41
+ )
42
+ process.exit(1)
43
+ }
44
+ return `${uploadDir.replace(/\/$/, "")}/${basename(file)}`
45
+ }
46
+
47
+ program
48
+ .name("yadisk")
49
+ .description("Yandex.Disk file management CLI")
50
+ .version("1.0.0")
51
+ .option("--username <username>", "Yandex username (overrides env)")
52
+ .option("--password <password>", "App password (overrides env)")
53
+ .option("--json", "Output as JSON")
54
+
55
+ // --- yadisk auth ---
56
+
57
+ program
58
+ .command("auth")
59
+ .description("Authenticate with username and app password")
60
+ .action(async () => {
61
+ const username = await prompt("Username: ")
62
+ if (!username) {
63
+ console.error("Error: No username provided.")
64
+ process.exit(1)
65
+ }
66
+
67
+ const password = await promptSecret("App password: ")
68
+ if (!password) {
69
+ console.error("Error: No app password provided.")
70
+ process.exit(1)
71
+ }
72
+
73
+ const credentials: Credentials = { username, password }
74
+ const spinner = ora("Validating credentials...").start()
75
+ try {
76
+ const client = new YaDiskClient(credentials)
77
+ await client.info()
78
+ spinner.succeed(`Authenticated as ${username}`)
79
+ } catch (err) {
80
+ spinner.fail("Authentication failed — check username and app password")
81
+ throw err
82
+ }
83
+
84
+ setConfigValue("username", username)
85
+ setConfigValue("password", password)
86
+ console.log("Credentials saved to ~/.config/yadisk/config.json")
87
+ })
88
+
89
+ // --- yadisk config ---
90
+
91
+ const configCmd = program
92
+ .command("config")
93
+ .description("Manage configuration")
94
+
95
+ configCmd
96
+ .command("set")
97
+ .description("Set a config value")
98
+ .argument("<key>", "Config key (username, password, upload_dir)")
99
+ .argument("<value>", "Config value")
100
+ .action((key: string, value: string) => {
101
+ if (!isValidConfigKey(key)) {
102
+ console.error(`Unknown config key: ${key}`)
103
+ console.error("Valid keys: username, password, upload_dir")
104
+ process.exit(1)
105
+ }
106
+ setConfigValue(key, value)
107
+ console.log(`${key} = ${key === "password" ? "***" : value}`)
108
+ })
109
+
110
+ configCmd
111
+ .command("get")
112
+ .description("Get a config value")
113
+ .argument("<key>", "Config key")
114
+ .action((key: string) => {
115
+ if (!isValidConfigKey(key)) {
116
+ console.error(`Unknown config key: ${key}`)
117
+ process.exit(1)
118
+ }
119
+ const value = getConfigValue(key)
120
+ if (value !== undefined) {
121
+ console.log(key === "password" ? "***" : value)
122
+ } else {
123
+ console.error(`${key} is not set`)
124
+ process.exit(1)
125
+ }
126
+ })
127
+
128
+ configCmd
129
+ .command("list")
130
+ .description("List all config values")
131
+ .action(() => {
132
+ const config = getConfig()
133
+ const entries = Object.entries(config)
134
+ if (entries.length === 0) {
135
+ console.log("No config values set.")
136
+ } else {
137
+ for (const [key, value] of entries) {
138
+ console.log(`${key} = ${key === "password" ? "***" : value}`)
139
+ }
140
+ }
141
+ })
142
+
143
+ configCmd
144
+ .command("unset")
145
+ .description("Remove a config value")
146
+ .argument("<key>", "Config key")
147
+ .action((key: string) => {
148
+ if (!isValidConfigKey(key)) {
149
+ console.error(`Unknown config key: ${key}`)
150
+ process.exit(1)
151
+ }
152
+ deleteConfigValue(key)
153
+ console.log(`Removed: ${key}`)
154
+ })
155
+
156
+ // --- yadisk info ---
157
+
158
+ program
159
+ .command("info")
160
+ .description("Show disk usage and capacity")
161
+ .action(async () => {
162
+ const client = getClient()
163
+ const disk = await client.info()
164
+
165
+ if (program.opts().json) {
166
+ console.log(formatJson(disk))
167
+ } else {
168
+ console.log(formatDiskInfo(disk))
169
+ }
170
+ })
171
+
172
+ // --- yadisk ls ---
173
+
174
+ program
175
+ .command("ls")
176
+ .description("List folder contents")
177
+ .argument("<path>", "Disk path (e.g. /uploads)")
178
+ .option("--sort <field>", "Sort field (name, size, modified)", "name")
179
+ .action(async (path: string, options) => {
180
+ const client = getClient()
181
+ const items = await client.list(path)
182
+
183
+ const sorted = sortResources(items, options.sort)
184
+
185
+ if (program.opts().json) {
186
+ console.log(formatJson(sorted))
187
+ } else {
188
+ console.log(formatResourceList(sorted))
189
+ console.log(chalk.dim(`\n${sorted.length} items`))
190
+ }
191
+ })
192
+
193
+ // --- yadisk stat ---
194
+
195
+ program
196
+ .command("stat")
197
+ .description("Show file/folder metadata")
198
+ .argument("<path>", "Disk path")
199
+ .action(async (path: string) => {
200
+ const client = getClient()
201
+ const resource = await client.stat(path)
202
+
203
+ if (program.opts().json) {
204
+ console.log(formatJson(resource))
205
+ } else {
206
+ console.log(formatResource(resource))
207
+ }
208
+ })
209
+
210
+ // --- yadisk mkdir ---
211
+
212
+ program
213
+ .command("mkdir")
214
+ .description("Create a folder")
215
+ .argument("<path>", "Disk path")
216
+ .action(async (path: string) => {
217
+ const client = getClient()
218
+ await client.mkdir(path)
219
+ console.log(`Created: ${path}`)
220
+ })
221
+
222
+ // --- yadisk upload ---
223
+
224
+ program
225
+ .command("upload")
226
+ .description("Upload a local file to Yandex.Disk")
227
+ .argument("<file>", "Local file path")
228
+ .argument("[dest]", "Destination disk path (default: <upload_dir>/<filename>)")
229
+ .option("--publish", "Publish after upload")
230
+ .action(async (file: string, dest: string | undefined, options) => {
231
+ const client = getClient()
232
+
233
+ const resolvedDest = resolveUploadDest(file, dest)
234
+ const fileObj = Bun.file(file)
235
+ const size = fileObj.size
236
+ const spinner = ora(`Uploading ${basename(file)} (${formatSize(size)})...`).start()
237
+
238
+ try {
239
+ await client.upload(resolvedDest, file)
240
+ spinner.succeed(`Uploaded: ${basename(file)} → ${resolvedDest}`)
241
+ } catch (err) {
242
+ spinner.fail("Upload failed")
243
+ throw err
244
+ }
245
+
246
+ if (options.publish) {
247
+ const url = await client.publish(resolvedDest)
248
+ const publicUrl = url ?? await client.getPublicUrl(resolvedDest)
249
+ console.log(`Public URL: ${publicUrl}`)
250
+ }
251
+ })
252
+
253
+ // --- yadisk download ---
254
+
255
+ program
256
+ .command("download")
257
+ .description("Download a file from Yandex.Disk")
258
+ .argument("<path>", "Disk path")
259
+ .argument("[dest]", "Local destination (default: current dir + filename)")
260
+ .action(async (path: string, dest?: string) => {
261
+ const client = getClient()
262
+ const localDest = dest ?? basename(path)
263
+
264
+ const spinner = ora(`Downloading ${basename(path)}...`).start()
265
+ try {
266
+ await client.download(path, localDest)
267
+ spinner.succeed(`Downloaded: ${path} → ${localDest}`)
268
+ } catch (err) {
269
+ spinner.fail("Download failed")
270
+ throw err
271
+ }
272
+ })
273
+
274
+ // --- yadisk cp ---
275
+
276
+ program
277
+ .command("cp")
278
+ .description("Copy a file or folder")
279
+ .argument("<from>", "Source disk path")
280
+ .argument("<to>", "Destination disk path")
281
+ .option("--overwrite", "Overwrite if exists")
282
+ .action(async (from: string, to: string, options) => {
283
+ const client = getClient()
284
+ await client.copy(from, to, options.overwrite)
285
+ console.log(`Copied: ${from} → ${to}`)
286
+ })
287
+
288
+ // --- yadisk mv ---
289
+
290
+ program
291
+ .command("mv")
292
+ .description("Move or rename a file or folder")
293
+ .argument("<from>", "Source disk path")
294
+ .argument("<to>", "Destination disk path")
295
+ .option("--overwrite", "Overwrite if exists")
296
+ .action(async (from: string, to: string, options) => {
297
+ const client = getClient()
298
+ await client.move(from, to, options.overwrite)
299
+ console.log(`Moved: ${from} → ${to}`)
300
+ })
301
+
302
+ // --- yadisk rm ---
303
+
304
+ program
305
+ .command("rm")
306
+ .description("Delete a file or folder")
307
+ .argument("<path>", "Disk path")
308
+ .action(async (path: string) => {
309
+ const client = getClient()
310
+ await client.delete(path)
311
+ console.log(`Deleted: ${path}`)
312
+ })
313
+
314
+ // --- yadisk publish ---
315
+
316
+ program
317
+ .command("publish")
318
+ .description("Publish a resource and get public URL")
319
+ .argument("<path>", "Disk path")
320
+ .action(async (path: string) => {
321
+ const client = getClient()
322
+ const url = await client.publish(path)
323
+ if (url) {
324
+ console.log(url)
325
+ } else {
326
+ const existing = await client.getPublicUrl(path)
327
+ console.log(existing ?? "Published (no URL returned)")
328
+ }
329
+ })
330
+
331
+ // --- yadisk unpublish ---
332
+
333
+ program
334
+ .command("unpublish")
335
+ .description("Remove public access from a resource")
336
+ .argument("<path>", "Disk path")
337
+ .action(async (path: string) => {
338
+ const client = getClient()
339
+ await client.unpublish(path)
340
+ console.log(`Unpublished: ${path}`)
341
+ })
342
+
343
+ // --- Helpers ---
344
+
345
+ function prompt(message: string): Promise<string> {
346
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
347
+ return new Promise((resolve) => {
348
+ rl.question(message, (answer) => {
349
+ rl.close()
350
+ resolve(answer.trim())
351
+ })
352
+ })
353
+ }
354
+
355
+ function promptSecret(message: string): Promise<string> {
356
+ process.stdout.write(message)
357
+ return new Promise((resolve) => {
358
+ const rl = createInterface({ input: process.stdin, terminal: false })
359
+ process.stdin.setRawMode?.(true)
360
+ let input = ""
361
+ const onData = (ch: Buffer) => {
362
+ const c = ch.toString()
363
+ if (c === "\n" || c === "\r") {
364
+ process.stdin.setRawMode?.(false)
365
+ process.stdin.removeListener("data", onData)
366
+ rl.close()
367
+ process.stdout.write("\n")
368
+ resolve(input.trim())
369
+ } else if (c === "\x7f" || c === "\b") {
370
+ input = input.slice(0, -1)
371
+ } else if (c === "\x03") {
372
+ process.exit(130)
373
+ } else {
374
+ input += c
375
+ }
376
+ }
377
+ process.stdin.on("data", onData)
378
+ })
379
+ }
380
+
381
+ function sortResources(items: Resource[], field: string): Resource[] {
382
+ const sorted = [...items]
383
+ switch (field) {
384
+ case "name":
385
+ sorted.sort((a, b) => a.name.localeCompare(b.name))
386
+ break
387
+ case "size":
388
+ sorted.sort((a, b) => (a.size ?? 0) - (b.size ?? 0))
389
+ break
390
+ case "modified":
391
+ sorted.sort((a, b) => new Date(a.modified).getTime() - new Date(b.modified).getTime())
392
+ break
393
+ case "-name":
394
+ sorted.sort((a, b) => b.name.localeCompare(a.name))
395
+ break
396
+ case "-size":
397
+ sorted.sort((a, b) => (b.size ?? 0) - (a.size ?? 0))
398
+ break
399
+ case "-modified":
400
+ sorted.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime())
401
+ break
402
+ }
403
+ return sorted
404
+ }
405
+
406
+ // --- Run ---
407
+
408
+ program.parseAsync().catch((err: Error) => {
409
+ console.error(chalk.red(`Error: ${err.message}`))
410
+ process.exit(1)
411
+ })
package/src/format.ts ADDED
@@ -0,0 +1,119 @@
1
+ import type { DiskInfo, Resource } from "@vforsh/yadisk"
2
+
3
+ // --- Size Formatting ---
4
+
5
+ const SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]
6
+
7
+ export function formatSize(bytes: number): string {
8
+ let i = 0
9
+ let size = bytes
10
+ while (size >= 1024 && i < SIZE_UNITS.length - 1) {
11
+ size /= 1024
12
+ i++
13
+ }
14
+ return i === 0 ? `${size} ${SIZE_UNITS[i]}` : `${size.toFixed(1)} ${SIZE_UNITS[i]}`
15
+ }
16
+
17
+ // --- Table Renderer ---
18
+
19
+ interface Column<T> {
20
+ header: string
21
+ value: keyof T | ((item: T) => string)
22
+ width?: number
23
+ }
24
+
25
+ function renderTable<T>(items: T[], columns: Column<T>[]): string {
26
+ const widths = columns.map((col) => {
27
+ const headerLen = col.header.length
28
+ const maxDataLen = items.reduce((max, item) => {
29
+ const val =
30
+ typeof col.value === "function"
31
+ ? col.value(item)
32
+ : String(item[col.value] ?? "")
33
+ return Math.max(max, val.length)
34
+ }, 0)
35
+ return col.width ?? Math.max(headerLen, maxDataLen)
36
+ })
37
+
38
+ const header = columns
39
+ .map((col, i) => col.header.padEnd(widths[i]))
40
+ .join(" ")
41
+ const separator = widths.map((w) => "-".repeat(w)).join(" ")
42
+ const rows = items.map((item) =>
43
+ columns
44
+ .map((col, i) => {
45
+ const val =
46
+ typeof col.value === "function"
47
+ ? col.value(item)
48
+ : String(item[col.value] ?? "")
49
+ return val.padEnd(widths[i])
50
+ })
51
+ .join(" ")
52
+ )
53
+
54
+ return [header, separator, ...rows].join("\n")
55
+ }
56
+
57
+ // --- Formatters ---
58
+
59
+ export function formatDiskInfo(disk: DiskInfo): string {
60
+ const total = formatSize(disk.total_bytes)
61
+ const used = formatSize(disk.used_bytes)
62
+ const free = formatSize(disk.available_bytes)
63
+ const pct = ((disk.used_bytes / disk.total_bytes) * 100).toFixed(1)
64
+
65
+ return [
66
+ `Total: ${total}`,
67
+ `Used: ${used} (${pct}%)`,
68
+ `Free: ${free}`,
69
+ ].join("\n")
70
+ }
71
+
72
+ export function formatResourceList(items: Resource[]): string {
73
+ if (items.length === 0) return "Empty folder."
74
+ return renderTable(items, [
75
+ {
76
+ header: "Type",
77
+ value: (r) => (r.type === "dir" ? "dir" : r.content_type ?? "file"),
78
+ width: 6,
79
+ },
80
+ {
81
+ header: "Size",
82
+ value: (r) => (r.size !== undefined ? formatSize(r.size) : "-"),
83
+ width: 10,
84
+ },
85
+ {
86
+ header: "Modified",
87
+ value: (r) => formatDate(r.modified),
88
+ width: 19,
89
+ },
90
+ { header: "Name", value: "name" },
91
+ ])
92
+ }
93
+
94
+ export function formatResource(resource: Resource): string {
95
+ const type = resource.content_type ?? resource.type
96
+ const lines = [
97
+ `Name: ${resource.name}`,
98
+ `Path: ${resource.path}`,
99
+ `Type: ${type}`,
100
+ ]
101
+ if (resource.size !== undefined) lines.push(`Size: ${formatSize(resource.size)}`)
102
+ lines.push(`Created: ${formatDate(resource.created)}`)
103
+ lines.push(`Modified: ${formatDate(resource.modified)}`)
104
+ if (resource.etag) lines.push(`ETag: ${resource.etag}`)
105
+ return lines.join("\n")
106
+ }
107
+
108
+ export function formatJson(data: unknown): string {
109
+ return JSON.stringify(data, null, 2)
110
+ }
111
+
112
+ function formatDate(dateStr: string): string {
113
+ if (!dateStr) return "-"
114
+ try {
115
+ return new Date(dateStr).toISOString().replace("T", " ").slice(0, 19)
116
+ } catch {
117
+ return dateStr.slice(0, 19)
118
+ }
119
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "composite": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "types": ["bun"]
12
+ },
13
+ "include": ["src/**/*"],
14
+ "references": [{ "path": "../yadisk" }]
15
+ }