@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 +19 -0
- package/package.json.bak +19 -0
- package/src/cli.ts +411 -0
- package/src/format.ts +119 -0
- package/tsconfig.json +15 -0
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
|
+
}
|
package/package.json.bak
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": "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
|
+
}
|