dexto 1.14.6 → 2.0.0-beta.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.
Files changed (297) hide show
  1. package/dist/agents/coding-agent/coding-agent.yml +0 -2
  2. package/dist/cli/agent-audit.d.ts +12 -0
  3. package/dist/cli/agent-audit.d.ts.map +1 -0
  4. package/dist/cli/agent-audit.js +52 -0
  5. package/dist/cli/auth/billing.d.ts +0 -6
  6. package/dist/cli/auth/billing.d.ts.map +1 -1
  7. package/dist/cli/auth/billing.js +0 -23
  8. package/dist/cli/auth/constants.d.ts +3 -31
  9. package/dist/cli/auth/constants.d.ts.map +1 -1
  10. package/dist/cli/auth/constants.js +9 -33
  11. package/dist/cli/auth/index.d.ts +2 -9
  12. package/dist/cli/auth/index.d.ts.map +1 -1
  13. package/dist/cli/auth/index.js +2 -7
  14. package/dist/cli/auth/sign-in-runtime.d.ts +12 -0
  15. package/dist/cli/auth/sign-in-runtime.d.ts.map +1 -0
  16. package/dist/cli/auth/sign-in-runtime.js +59 -0
  17. package/dist/cli/cloud/app-scopes.d.ts +4 -0
  18. package/dist/cli/cloud/app-scopes.d.ts.map +1 -0
  19. package/dist/cli/cloud/app-scopes.js +19 -0
  20. package/dist/cli/cloud/command.d.ts +107 -0
  21. package/dist/cli/cloud/command.d.ts.map +1 -0
  22. package/dist/cli/cloud/command.js +154 -0
  23. package/dist/cli/cloud/commands/account.d.ts +11 -0
  24. package/dist/cli/cloud/commands/account.d.ts.map +1 -0
  25. package/dist/cli/cloud/commands/account.js +129 -0
  26. package/dist/cli/cloud/commands/agents.d.ts +3 -0
  27. package/dist/cli/cloud/commands/agents.d.ts.map +1 -0
  28. package/dist/cli/cloud/commands/agents.js +526 -0
  29. package/dist/cli/cloud/commands/api.d.ts +3 -0
  30. package/dist/cli/cloud/commands/api.d.ts.map +1 -0
  31. package/dist/cli/cloud/commands/api.js +26 -0
  32. package/dist/cli/cloud/commands/app-files.d.ts +29 -0
  33. package/dist/cli/cloud/commands/app-files.d.ts.map +1 -0
  34. package/dist/cli/cloud/commands/app-files.js +72 -0
  35. package/dist/cli/cloud/commands/artifacts.d.ts +3 -0
  36. package/dist/cli/cloud/commands/artifacts.d.ts.map +1 -0
  37. package/dist/cli/cloud/commands/artifacts.js +267 -0
  38. package/dist/cli/cloud/commands/automations.d.ts +3 -0
  39. package/dist/cli/cloud/commands/automations.d.ts.map +1 -0
  40. package/dist/cli/cloud/commands/automations.js +233 -0
  41. package/dist/cli/cloud/commands/chats.d.ts +3 -0
  42. package/dist/cli/cloud/commands/chats.d.ts.map +1 -0
  43. package/dist/cli/cloud/commands/chats.js +204 -0
  44. package/dist/cli/cloud/commands/data-deletions.d.ts +3 -0
  45. package/dist/cli/cloud/commands/data-deletions.d.ts.map +1 -0
  46. package/dist/cli/cloud/commands/data-deletions.js +55 -0
  47. package/dist/cli/cloud/commands/deploys.d.ts +10 -0
  48. package/dist/cli/cloud/commands/deploys.d.ts.map +1 -0
  49. package/dist/cli/cloud/commands/deploys.js +225 -0
  50. package/dist/cli/cloud/commands/drive.d.ts +12 -0
  51. package/dist/cli/cloud/commands/drive.d.ts.map +1 -0
  52. package/dist/cli/cloud/commands/drive.js +321 -0
  53. package/dist/cli/cloud/commands/files.d.ts +24 -0
  54. package/dist/cli/cloud/commands/files.d.ts.map +1 -0
  55. package/dist/cli/cloud/commands/files.js +284 -0
  56. package/dist/cli/cloud/commands/find.d.ts +3 -0
  57. package/dist/cli/cloud/commands/find.d.ts.map +1 -0
  58. package/dist/cli/cloud/commands/find.js +63 -0
  59. package/dist/cli/cloud/commands/models.d.ts +3 -0
  60. package/dist/cli/cloud/commands/models.d.ts.map +1 -0
  61. package/dist/cli/cloud/commands/models.js +250 -0
  62. package/dist/cli/cloud/commands/profile.d.ts +3 -0
  63. package/dist/cli/cloud/commands/profile.d.ts.map +1 -0
  64. package/dist/cli/cloud/commands/profile.js +120 -0
  65. package/dist/cli/cloud/commands/projects.d.ts +5 -0
  66. package/dist/cli/cloud/commands/projects.d.ts.map +1 -0
  67. package/dist/cli/cloud/commands/projects.js +66 -0
  68. package/dist/cli/cloud/commands/publish.d.ts +3 -0
  69. package/dist/cli/cloud/commands/publish.d.ts.map +1 -0
  70. package/dist/cli/cloud/commands/publish.js +139 -0
  71. package/dist/cli/cloud/commands/sandboxes.d.ts +3 -0
  72. package/dist/cli/cloud/commands/sandboxes.d.ts.map +1 -0
  73. package/dist/cli/cloud/commands/sandboxes.js +266 -0
  74. package/dist/cli/cloud/commands/secrets.d.ts +3 -0
  75. package/dist/cli/cloud/commands/secrets.d.ts.map +1 -0
  76. package/dist/cli/cloud/commands/secrets.js +111 -0
  77. package/dist/cli/cloud/commands/skills.d.ts +3 -0
  78. package/dist/cli/cloud/commands/skills.d.ts.map +1 -0
  79. package/dist/cli/cloud/commands/skills.js +203 -0
  80. package/dist/cli/cloud/commands/traces.d.ts +3 -0
  81. package/dist/cli/cloud/commands/traces.d.ts.map +1 -0
  82. package/dist/cli/cloud/commands/traces.js +142 -0
  83. package/dist/cli/cloud/commands/web.d.ts +3 -0
  84. package/dist/cli/cloud/commands/web.d.ts.map +1 -0
  85. package/dist/cli/cloud/commands/web.js +53 -0
  86. package/dist/cli/cloud/commands/workspace-apps.d.ts +58 -0
  87. package/dist/cli/cloud/commands/workspace-apps.d.ts.map +1 -0
  88. package/dist/cli/cloud/commands/workspace-apps.js +657 -0
  89. package/dist/cli/cloud/commands/workspace.d.ts +36 -0
  90. package/dist/cli/cloud/commands/workspace.d.ts.map +1 -0
  91. package/dist/cli/cloud/commands/workspace.js +281 -0
  92. package/dist/cli/cloud/connection.d.ts +33 -0
  93. package/dist/cli/cloud/connection.d.ts.map +1 -0
  94. package/dist/cli/cloud/connection.js +38 -0
  95. package/dist/cli/cloud/events.d.ts +63 -0
  96. package/dist/cli/cloud/events.d.ts.map +1 -0
  97. package/dist/cli/cloud/events.js +72 -0
  98. package/dist/cli/cloud/host.d.ts +9 -0
  99. package/dist/cli/cloud/host.d.ts.map +1 -0
  100. package/dist/cli/cloud/host.js +109 -0
  101. package/dist/cli/cloud/output.d.ts +34 -0
  102. package/dist/cli/cloud/output.d.ts.map +1 -0
  103. package/dist/cli/cloud/output.js +77 -0
  104. package/dist/cli/cloud/run.d.ts +26 -0
  105. package/dist/cli/cloud/run.d.ts.map +1 -0
  106. package/dist/cli/cloud/run.js +444 -0
  107. package/dist/cli/cloud/runtime.d.ts +33 -0
  108. package/dist/cli/cloud/runtime.d.ts.map +1 -0
  109. package/dist/cli/cloud/runtime.js +86 -0
  110. package/dist/cli/cloud/tar.d.ts +9 -0
  111. package/dist/cli/cloud/tar.d.ts.map +1 -0
  112. package/dist/cli/cloud/tar.js +69 -0
  113. package/dist/cli/cloud/transfer.d.ts +13 -0
  114. package/dist/cli/cloud/transfer.d.ts.map +1 -0
  115. package/dist/cli/cloud/transfer.js +36 -0
  116. package/dist/cli/cloud/v1.d.ts +20 -0
  117. package/dist/cli/cloud/v1.d.ts.map +1 -0
  118. package/dist/cli/cloud/v1.js +66 -0
  119. package/dist/cli/cloud/zip.d.ts +8 -0
  120. package/dist/cli/cloud/zip.d.ts.map +1 -0
  121. package/dist/cli/cloud/zip.js +66 -0
  122. package/dist/cli/commands/auth/sign-in-commands.d.ts +62 -0
  123. package/dist/cli/commands/auth/sign-in-commands.d.ts.map +1 -0
  124. package/dist/cli/commands/auth/sign-in-commands.js +248 -0
  125. package/dist/cli/commands/auth/sign-in-context.d.ts +16 -0
  126. package/dist/cli/commands/auth/sign-in-context.d.ts.map +1 -0
  127. package/dist/cli/commands/auth/sign-in-context.js +68 -0
  128. package/dist/cli/commands/cloud/register.d.ts +5 -0
  129. package/dist/cli/commands/cloud/register.d.ts.map +1 -1
  130. package/dist/cli/commands/cloud/register.js +44 -24
  131. package/dist/cli/commands/create-app.js +5 -6
  132. package/dist/cli/commands/create-image.d.ts.map +1 -1
  133. package/dist/cli/commands/create-image.js +9 -10
  134. package/dist/cli/commands/index.d.ts +0 -2
  135. package/dist/cli/commands/index.d.ts.map +1 -1
  136. package/dist/cli/commands/index.js +0 -2
  137. package/dist/cli/commands/init.d.ts +0 -2
  138. package/dist/cli/commands/init.d.ts.map +1 -1
  139. package/dist/cli/commands/init.js +5 -26
  140. package/dist/cli/commands/run/headless.d.ts.map +1 -1
  141. package/dist/cli/commands/run/headless.js +57 -2
  142. package/dist/cli/commands/run/output.d.ts +29 -10
  143. package/dist/cli/commands/run/output.d.ts.map +1 -1
  144. package/dist/cli/commands/run/output.js +44 -22
  145. package/dist/cli/commands/run/register.d.ts.map +1 -1
  146. package/dist/cli/commands/run/register.js +21 -9
  147. package/dist/cli/commands/run/step-log.d.ts +11 -0
  148. package/dist/cli/commands/run/step-log.d.ts.map +1 -0
  149. package/dist/cli/commands/run/step-log.js +42 -0
  150. package/dist/cli/commands/session/register.d.ts.map +1 -1
  151. package/dist/cli/commands/session/register.js +51 -21
  152. package/dist/cli/commands/session-commands.js +1 -1
  153. package/dist/cli/commands/setup.js +17 -14
  154. package/dist/cli/failure.d.ts +29 -0
  155. package/dist/cli/failure.d.ts.map +1 -0
  156. package/dist/cli/failure.js +31 -0
  157. package/dist/cli/help-json.d.ts +22 -0
  158. package/dist/cli/help-json.d.ts.map +1 -0
  159. package/dist/cli/help-json.js +20 -0
  160. package/dist/cli/import/claude-code.d.ts +10 -0
  161. package/dist/cli/import/claude-code.d.ts.map +1 -0
  162. package/dist/cli/import/claude-code.js +131 -0
  163. package/dist/cli/import/codex.d.ts +27 -0
  164. package/dist/cli/import/codex.d.ts.map +1 -0
  165. package/dist/cli/import/codex.js +148 -0
  166. package/dist/cli/import/git.d.ts +19 -0
  167. package/dist/cli/import/git.d.ts.map +1 -0
  168. package/dist/cli/import/git.js +95 -0
  169. package/dist/cli/import/local-chats.d.ts +28 -0
  170. package/dist/cli/import/local-chats.d.ts.map +1 -0
  171. package/dist/cli/import/local-chats.js +129 -0
  172. package/dist/cli/import/redaction.d.ts +37 -0
  173. package/dist/cli/import/redaction.d.ts.map +1 -0
  174. package/dist/cli/import/redaction.js +148 -0
  175. package/dist/cli/import/test-utils.d.ts +16 -0
  176. package/dist/cli/import/test-utils.d.ts.map +1 -0
  177. package/dist/cli/import/test-utils.js +223 -0
  178. package/dist/cli/import/transcript.d.ts +75 -0
  179. package/dist/cli/import/transcript.d.ts.map +1 -0
  180. package/dist/cli/import/transcript.js +123 -0
  181. package/dist/cli/interaction.d.ts +18 -0
  182. package/dist/cli/interaction.d.ts.map +1 -0
  183. package/dist/cli/interaction.js +30 -0
  184. package/dist/cli/modes/cli.d.ts.map +1 -1
  185. package/dist/cli/modes/cli.js +42 -9
  186. package/dist/cli/modes/server.js +8 -8
  187. package/dist/cli/modes/web.js +3 -3
  188. package/dist/cli/output.d.ts +9 -0
  189. package/dist/cli/output.d.ts.map +1 -0
  190. package/dist/cli/output.js +6 -0
  191. package/dist/cli/utils/dexto-auth-check.d.ts +1 -1
  192. package/dist/cli/utils/dexto-auth-check.d.ts.map +1 -1
  193. package/dist/cli/utils/dexto-auth-check.js +4 -5
  194. package/dist/cli/utils/dexto-setup.d.ts +4 -5
  195. package/dist/cli/utils/dexto-setup.d.ts.map +1 -1
  196. package/dist/cli/utils/dexto-setup.js +17 -14
  197. package/dist/cli/utils/provider-setup.js +1 -1
  198. package/dist/cli/utils/scaffolding-utils.d.ts +9 -3
  199. package/dist/cli/utils/scaffolding-utils.d.ts.map +1 -1
  200. package/dist/cli/utils/scaffolding-utils.js +48 -40
  201. package/dist/cli/utils/version-check.d.ts.map +1 -1
  202. package/dist/cli/utils/version-check.js +2 -1
  203. package/dist/index-main.js +44 -65
  204. package/dist/webui/assets/index-D9xiOs62.js +1649 -0
  205. package/dist/webui/index.html +1 -1
  206. package/package.json +15 -14
  207. package/dist/cli/auth/api-client.d.ts +0 -125
  208. package/dist/cli/auth/api-client.d.ts.map +0 -1
  209. package/dist/cli/auth/api-client.js +0 -585
  210. package/dist/cli/auth/device.d.ts +0 -14
  211. package/dist/cli/auth/device.d.ts.map +0 -1
  212. package/dist/cli/auth/device.js +0 -84
  213. package/dist/cli/auth/dexto-api-key.d.ts +0 -34
  214. package/dist/cli/auth/dexto-api-key.d.ts.map +0 -1
  215. package/dist/cli/auth/dexto-api-key.js +0 -160
  216. package/dist/cli/auth/login-persistence.d.ts +0 -15
  217. package/dist/cli/auth/login-persistence.d.ts.map +0 -1
  218. package/dist/cli/auth/login-persistence.js +0 -72
  219. package/dist/cli/auth/oauth.d.ts +0 -39
  220. package/dist/cli/auth/oauth.d.ts.map +0 -1
  221. package/dist/cli/auth/oauth.js +0 -434
  222. package/dist/cli/auth/service.d.ts +0 -24
  223. package/dist/cli/auth/service.d.ts.map +0 -1
  224. package/dist/cli/auth/service.js +0 -161
  225. package/dist/cli/auth/types.d.ts +0 -11
  226. package/dist/cli/auth/types.d.ts.map +0 -1
  227. package/dist/cli/auth/types.js +0 -1
  228. package/dist/cli/cloud-chat.d.ts +0 -18
  229. package/dist/cli/cloud-chat.d.ts.map +0 -1
  230. package/dist/cli/cloud-chat.js +0 -734
  231. package/dist/cli/commands/auth/index.d.ts +0 -4
  232. package/dist/cli/commands/auth/index.d.ts.map +0 -1
  233. package/dist/cli/commands/auth/index.js +0 -4
  234. package/dist/cli/commands/auth/login.d.ts +0 -12
  235. package/dist/cli/commands/auth/login.d.ts.map +0 -1
  236. package/dist/cli/commands/auth/login.js +0 -153
  237. package/dist/cli/commands/auth/logout.d.ts +0 -5
  238. package/dist/cli/commands/auth/logout.d.ts.map +0 -1
  239. package/dist/cli/commands/auth/logout.js +0 -81
  240. package/dist/cli/commands/auth/register.d.ts +0 -3
  241. package/dist/cli/commands/auth/register.d.ts.map +0 -1
  242. package/dist/cli/commands/auth/register.js +0 -96
  243. package/dist/cli/commands/auth/status.d.ts +0 -2
  244. package/dist/cli/commands/auth/status.d.ts.map +0 -1
  245. package/dist/cli/commands/auth/status.js +0 -22
  246. package/dist/cli/commands/billing/index.d.ts +0 -2
  247. package/dist/cli/commands/billing/index.d.ts.map +0 -1
  248. package/dist/cli/commands/billing/index.js +0 -2
  249. package/dist/cli/commands/billing/register.d.ts +0 -3
  250. package/dist/cli/commands/billing/register.d.ts.map +0 -1
  251. package/dist/cli/commands/billing/register.js +0 -20
  252. package/dist/cli/commands/billing/status.d.ts +0 -8
  253. package/dist/cli/commands/billing/status.d.ts.map +0 -1
  254. package/dist/cli/commands/billing/status.js +0 -71
  255. package/dist/cli/commands/cloud/discovery.d.ts +0 -17
  256. package/dist/cli/commands/cloud/discovery.d.ts.map +0 -1
  257. package/dist/cli/commands/cloud/discovery.js +0 -88
  258. package/dist/cli/commands/deploy/client.d.ts +0 -113
  259. package/dist/cli/commands/deploy/client.d.ts.map +0 -1
  260. package/dist/cli/commands/deploy/client.js +0 -519
  261. package/dist/cli/commands/deploy/config.d.ts +0 -37
  262. package/dist/cli/commands/deploy/config.d.ts.map +0 -1
  263. package/dist/cli/commands/deploy/config.js +0 -144
  264. package/dist/cli/commands/deploy/entry-agent.d.ts +0 -3
  265. package/dist/cli/commands/deploy/entry-agent.d.ts.map +0 -1
  266. package/dist/cli/commands/deploy/entry-agent.js +0 -70
  267. package/dist/cli/commands/deploy/index.d.ts +0 -12
  268. package/dist/cli/commands/deploy/index.d.ts.map +0 -1
  269. package/dist/cli/commands/deploy/index.js +0 -370
  270. package/dist/cli/commands/deploy/links.d.ts +0 -3
  271. package/dist/cli/commands/deploy/links.d.ts.map +0 -1
  272. package/dist/cli/commands/deploy/links.js +0 -53
  273. package/dist/cli/commands/deploy/register.d.ts +0 -6
  274. package/dist/cli/commands/deploy/register.d.ts.map +0 -1
  275. package/dist/cli/commands/deploy/register.js +0 -128
  276. package/dist/cli/commands/deploy/snapshot.d.ts +0 -12
  277. package/dist/cli/commands/deploy/snapshot.d.ts.map +0 -1
  278. package/dist/cli/commands/deploy/snapshot.js +0 -76
  279. package/dist/cli/commands/deploy/state.d.ts +0 -13
  280. package/dist/cli/commands/deploy/state.d.ts.map +0 -1
  281. package/dist/cli/commands/deploy/state.js +0 -122
  282. package/dist/cli/commands/span/register.d.ts +0 -6
  283. package/dist/cli/commands/span/register.d.ts.map +0 -1
  284. package/dist/cli/commands/span/register.js +0 -25
  285. package/dist/cli/commands/trace/client.d.ts +0 -98
  286. package/dist/cli/commands/trace/client.d.ts.map +0 -1
  287. package/dist/cli/commands/trace/client.js +0 -150
  288. package/dist/cli/commands/trace/format.d.ts +0 -5
  289. package/dist/cli/commands/trace/format.d.ts.map +0 -1
  290. package/dist/cli/commands/trace/format.js +0 -58
  291. package/dist/cli/commands/trace/index.d.ts +0 -21
  292. package/dist/cli/commands/trace/index.d.ts.map +0 -1
  293. package/dist/cli/commands/trace/index.js +0 -49
  294. package/dist/cli/commands/trace/register.d.ts +0 -6
  295. package/dist/cli/commands/trace/register.d.ts.map +0 -1
  296. package/dist/cli/commands/trace/register.js +0 -71
  297. package/dist/webui/assets/index-DEfRFmFT.js +0 -1667
@@ -0,0 +1,321 @@
1
+ // `drive put`: uploads local files and folder trees to Drive, versioning same-named files.
2
+ import { CommandFailure } from '../../failure.js';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { openAsBlob } from 'node:fs';
5
+ import { lstat, readdir, stat } from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { z } from 'zod';
8
+ import { isJsonObject, stringOption, UsageError, } from '../command.js';
9
+ import { sendBytes } from '../transfer.js';
10
+ import { contentTypeFor, formatBytes, hashFile } from './files.js';
11
+ import { queryWorkspaceTool } from './workspace.js';
12
+ /** How many files upload at once. */
13
+ const CONCURRENCY = 4;
14
+ /** How many times a part, or the completion, is sent before a network failure gives it up. */
15
+ const PART_ATTEMPTS = 3;
16
+ /** How long giving an upload up may take, even after the command was cancelled. */
17
+ const ABORT_TIMEOUT_MS = 10_000;
18
+ /** A Drive item as a list describes it. */
19
+ const ItemSchema = z
20
+ .object({
21
+ id: z.string(),
22
+ kind: z.string(),
23
+ name: z.string(),
24
+ parentId: z.string().nullable(),
25
+ revision: z.number(),
26
+ sha256: z.string().nullable().default(null),
27
+ size: z.number().nullable().default(null),
28
+ trashedAt: z.string().nullable().default(null),
29
+ })
30
+ .passthrough();
31
+ const UsageSchema = z.object({ limits: z.object({ fileBytes: z.number() }) });
32
+ const PageSchema = z.object({ items: z.array(ItemSchema), nextOffset: z.number().nullable() });
33
+ const FolderSchema = z.object({ item: z.object({ id: z.string() }).passthrough() });
34
+ const Endpoint = z.object({ method: z.string(), url: z.string() });
35
+ /** Where a reserved upload goes: one URL, or, for a large file, its parts and their completion. */
36
+ const ReservedSchema = z.object({
37
+ upload: z.union([
38
+ z.object({
39
+ multipart: z.object({
40
+ abort: Endpoint,
41
+ complete: Endpoint,
42
+ partCount: z.number().int().positive(),
43
+ partSize: z.number().int().positive(),
44
+ parts: Endpoint,
45
+ }),
46
+ }),
47
+ Endpoint,
48
+ ]),
49
+ });
50
+ const put = {
51
+ arguments: '<paths...>',
52
+ options: {
53
+ to: {
54
+ description: 'The Drive folder to put them in, by id (default: the top of your Drive).',
55
+ type: 'string',
56
+ valueName: 'FOLDER_ID',
57
+ },
58
+ },
59
+ path: ['workspace', 'drive', 'put'],
60
+ summary: 'Upload local files and folders (dotfiles left out) to your Drive; a file already there by name gets a new version, unless its bytes are the same.',
61
+ async run(context, input) {
62
+ const to = stringOption(input, 'to') ?? null;
63
+ const { files, folders } = await collect(input.positionals);
64
+ const usage = UsageSchema.parse(await queryWorkspaceTool(context, 'dexto_drive', { action: 'usage' }));
65
+ const limit = usage.limits.fileBytes;
66
+ const tooLarge = files.filter((file) => file.size > limit);
67
+ if (tooLarge.length > 0)
68
+ throw new CommandFailure('invalid_input', `Drive takes files up to ${formatBytes(limit)}, and ${tooLarge
69
+ .map((file) => `${file.localPath} is ${formatBytes(file.size)}`)
70
+ .join(', ')}. Leave ${tooLarge.length === 1 ? 'it' : 'them'} out and run it again.`, null);
71
+ // Listing the destination first fails the run at once when it isn't a folder you can use.
72
+ const listed = new Map();
73
+ await listFolder(context, listed, to);
74
+ // Parents come before their children, so each folder's parent is there by the time it is.
75
+ const folderIds = new Map();
76
+ const putFolders = [];
77
+ for (const folder of folders) {
78
+ const parentId = folder.parent === null ? to : (folderIds.get(folder.parent) ?? null);
79
+ const name = path.basename(path.resolve(folder.localPath));
80
+ const existing = await findItem(context, listed, parentId, 'folder', name);
81
+ const item = existing ??
82
+ FolderSchema.parse(await queryWorkspaceTool(context, 'dexto_drive', {
83
+ action: 'folder',
84
+ name,
85
+ parentId,
86
+ requestId: randomUUID(),
87
+ })).item;
88
+ // A folder made just now has nothing in it to list.
89
+ if (existing === null)
90
+ listed.set(item.id, Promise.resolve([]));
91
+ folderIds.set(folder.localPath, item.id);
92
+ putFolders.push({
93
+ item,
94
+ path: folder.localPath,
95
+ status: existing === null ? 'created' : 'existing',
96
+ });
97
+ }
98
+ const saved = [];
99
+ const failed = [];
100
+ // Files bound for one name in one folder go one after another, each a version of the last.
101
+ const bound = new Map();
102
+ for (const file of files) {
103
+ const parentId = file.folder === null ? to : (folderIds.get(file.folder) ?? null);
104
+ const key = `${parentId ?? ''}/${path.basename(file.localPath)}`;
105
+ bound.set(key, [...(bound.get(key) ?? []), file]);
106
+ }
107
+ await eachAtOnce([...bound.values()], async (sameName) => {
108
+ for (const file of sameName) {
109
+ const parentId = file.folder === null ? to : (folderIds.get(file.folder) ?? null);
110
+ try {
111
+ const existing = await findItem(context, listed, parentId, 'file', path.basename(file.localPath));
112
+ const sha256 = await hashFile(file.localPath);
113
+ // Drive already has these bytes under this name: nothing to send.
114
+ if (existing?.sha256 === sha256 && existing.size === file.size) {
115
+ saved.push({ item: existing, path: file.localPath, status: 'unchanged' });
116
+ if (!context.output.json)
117
+ context.runtime.writeOutput(`Unchanged ${file.localPath} (${formatBytes(file.size)}, ${existing.id})\n`);
118
+ continue;
119
+ }
120
+ const item = await putFile(context, file, parentId, existing, sha256, null);
121
+ await remember(listed, parentId, item);
122
+ const status = existing === null ? 'created' : 'updated';
123
+ saved.push({ item, path: file.localPath, status });
124
+ if (!context.output.json)
125
+ context.runtime.writeOutput(`${status === 'created' ? 'Created' : 'Updated'} ${file.localPath} (${formatBytes(file.size)}, ${String(item.id)})\n`);
126
+ }
127
+ catch (error) {
128
+ // A sign-in that can't write fails every file the same way: stop, say so once.
129
+ if (error instanceof CommandFailure &&
130
+ (error.kind === 'needs_sign_in' || error.kind === 'needs_permission'))
131
+ throw error;
132
+ const message = error instanceof Error ? error.message : String(error);
133
+ failed.push({ error: message, path: file.localPath });
134
+ context.runtime.writeError(`Couldn't upload ${file.localPath}: ${message}\n`);
135
+ }
136
+ }
137
+ });
138
+ const created = saved.filter((each) => each.status === 'created').length;
139
+ const updated = saved.filter((each) => each.status === 'updated').length;
140
+ const unchanged = saved.length - created - updated;
141
+ const madeFolders = putFolders.filter((each) => each.status === 'created').length;
142
+ context.output.result({
143
+ created,
144
+ failed,
145
+ files: saved,
146
+ folders: putFolders,
147
+ unchanged,
148
+ updated,
149
+ }, `Uploaded ${created + updated} of ${files.length} file${files.length === 1 ? '' : 's'}: ${created} created, ${updated} updated with a new version${unchanged === 0 ? '' : `, ${unchanged} already up to date`}.${putFolders.length === 0
150
+ ? ''
151
+ : ` Folders: ${madeFolders} created, ${putFolders.length - madeFolders} already there.`}`);
152
+ if (failed.length > 0)
153
+ throw new CommandFailure('failed', `${failed.length} file${failed.length === 1 ? '' : 's'} didn't upload: ${failed.map((each) => each.path).join(', ')}.`, null);
154
+ },
155
+ };
156
+ /**
157
+ * The files and folders the paths name. A folder brings what's in it, except names starting with a
158
+ * dot and links, which are left out; a path named on its own is taken as it is.
159
+ */
160
+ async function collect(paths) {
161
+ const files = [];
162
+ const folders = [];
163
+ const visit = async (localPath, parent, named) => {
164
+ const stats = await (named ? stat(localPath) : lstat(localPath)).catch(() => null);
165
+ if (stats?.isFile() === true) {
166
+ files.push({ folder: parent, localPath, size: stats.size });
167
+ return;
168
+ }
169
+ if (stats?.isDirectory() !== true) {
170
+ if (named)
171
+ throw new UsageError(`${localPath} is not a file or folder.`);
172
+ return;
173
+ }
174
+ folders.push({ localPath, parent });
175
+ const names = (await readdir(localPath)).filter((name) => !name.startsWith('.')).sort();
176
+ for (const name of names)
177
+ await visit(path.join(localPath, name), localPath, false);
178
+ };
179
+ for (const localPath of paths)
180
+ await visit(localPath, null, true);
181
+ return { files, folders };
182
+ }
183
+ /** The item of this kind and name in a Drive folder (null for the top), listing it once a run. */
184
+ async function findItem(context, listed, parentId, kind, name) {
185
+ const items = await listFolder(context, listed, parentId);
186
+ return items.find((item) => item.kind === kind && item.name === name) ?? null;
187
+ }
188
+ /** Puts what an upload saved in its folder's listing, so the next file of that name versions it. */
189
+ async function remember(listed, parentId, saved) {
190
+ const item = ItemSchema.safeParse(saved);
191
+ if (!item.success)
192
+ return;
193
+ const key = parentId ?? '';
194
+ const items = (await listed.get(key)) ?? [];
195
+ listed.set(key, Promise.resolve([...items.filter((each) => each.id !== item.data.id), item.data]));
196
+ }
197
+ /** Every item in a Drive folder, page by page, trash left out; each folder is listed once. */
198
+ function listFolder(context, listed, parentId) {
199
+ const known = listed.get(parentId ?? '');
200
+ if (known !== undefined)
201
+ return known;
202
+ const listing = (async () => {
203
+ const items = [];
204
+ for (let offset = 0; offset !== null;) {
205
+ const page = PageSchema.parse(await queryWorkspaceTool(context, 'dexto_drive', {
206
+ action: 'list',
207
+ offset,
208
+ parentId,
209
+ }));
210
+ items.push(...page.items.filter((item) => item.trashedAt === null));
211
+ offset = page.nextOffset;
212
+ }
213
+ return items;
214
+ })();
215
+ listed.set(parentId ?? '', listing);
216
+ return listing;
217
+ }
218
+ /**
219
+ * Reserves one file in Drive under a new request id, as a new version of `existing` when there is
220
+ * one, then sends its bytes to the URL Drive returns. A file too large for one request would be
221
+ * sent in parts here, once Drive takes them.
222
+ */
223
+ async function putFile(context, file, parentId, existing, sha256,
224
+ /** The app whose own folder the file goes into instead, which gives the app the file. */
225
+ appId) {
226
+ const mimeType = contentTypeFor(file.localPath);
227
+ const reserved = ReservedSchema.safeParse(await queryWorkspaceTool(context, 'dexto_drive', {
228
+ action: 'upload',
229
+ ...(appId === null ? {} : { appId }),
230
+ mimeType,
231
+ name: path.basename(file.localPath),
232
+ requestId: randomUUID(),
233
+ sha256,
234
+ size: file.size,
235
+ ...(existing === null
236
+ ? { parentId }
237
+ : {
238
+ expectedRevision: existing.revision,
239
+ parentId: existing.parentId,
240
+ targetId: existing.id,
241
+ }),
242
+ }));
243
+ if (!reserved.success)
244
+ throw new CommandFailure('failed', "Drive didn't say where to send the file.", null);
245
+ const { upload } = reserved.data;
246
+ const bytes = await openAsBlob(file.localPath);
247
+ const reply = 'multipart' in upload
248
+ ? await sendParts(context, upload.multipart, bytes)
249
+ : await sendBytes(context, { headers: { 'content-type': mimeType }, method: upload.method, url: upload.url }, bytes);
250
+ return isJsonObject(reply) && isJsonObject(reply.item) ? reply.item : {};
251
+ }
252
+ /**
253
+ * Uploads one local file into an app's own Drive folder, which gives the app the file to refer to
254
+ * as `drive:<id>`.
255
+ */
256
+ export async function putAppFile(context, localPath, appId) {
257
+ const info = await stat(localPath).catch(() => null);
258
+ if (info === null || !info.isFile())
259
+ throw new UsageError(`Can't read ${localPath}: give a file's path, or a link.`);
260
+ const item = await putFile(context, { folder: null, localPath, size: info.size }, null, null, await hashFile(localPath), appId);
261
+ if (typeof item.id !== 'string')
262
+ throw new CommandFailure('failed', `Drive didn't return ${localPath}'s file.`, null);
263
+ return { id: item.id, mimeType: contentTypeFor(localPath), name: path.basename(localPath) };
264
+ }
265
+ /**
266
+ * Sends a large file in the exact parts Drive asked for, a few at once, then completes it. A part
267
+ * or completion the network drops is sent again; one that still fails, or is refused, gives the
268
+ * upload up at once, so it holds no quota until it expires, even when the command was cancelled.
269
+ */
270
+ async function sendParts(context, plan, bytes) {
271
+ const numbers = Array.from({ length: plan.partCount }, (_, index) => index + 1);
272
+ try {
273
+ await eachAtOnce(numbers, async (number) => {
274
+ await sendRetrying(context, { headers: {}, method: plan.parts.method, url: `${plan.parts.url}/${number}` }, bytes.slice((number - 1) * plan.partSize, number * plan.partSize), 1);
275
+ });
276
+ return await sendRetrying(context, { headers: {}, method: plan.complete.method, url: plan.complete.url }, new globalThis.Blob([]), 1);
277
+ }
278
+ catch (error) {
279
+ // Its own signal: the command's is already aborted when Ctrl-C ended the upload.
280
+ await context.runtime
281
+ .fetch(plan.abort.url, {
282
+ method: plan.abort.method,
283
+ signal: AbortSignal.timeout(ABORT_TIMEOUT_MS),
284
+ })
285
+ .catch(() => undefined);
286
+ throw error;
287
+ }
288
+ }
289
+ /**
290
+ * Sends a part or the completion, again after a second and then two when the network drops it.
291
+ * Drive keeps the last copy of a part sent twice and completes an upload once; a refusal stands.
292
+ */
293
+ async function sendRetrying(context, target, body, attempt) {
294
+ try {
295
+ return await sendBytes(context, target, body);
296
+ }
297
+ catch (error) {
298
+ // fetch rejects with a TypeError when the request never got a response.
299
+ if (!(error instanceof TypeError) || attempt === PART_ATTEMPTS)
300
+ throw error;
301
+ await context.runtime.sleep(1000 * attempt);
302
+ return await sendRetrying(context, target, body, attempt + 1);
303
+ }
304
+ }
305
+ /** Runs `run` over the items, a few at once; the first error it throws stops the rest. */
306
+ async function eachAtOnce(items, run) {
307
+ let next = 0;
308
+ const worker = async () => {
309
+ for (let item = items[next++]; item !== undefined; item = items[next++]) {
310
+ try {
311
+ await run(item);
312
+ }
313
+ catch (error) {
314
+ next = items.length;
315
+ throw error;
316
+ }
317
+ }
318
+ };
319
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, worker));
320
+ }
321
+ export const driveCommands = [put];
@@ -0,0 +1,24 @@
1
+ import { type V1Response } from '@dexto/cloud-client';
2
+ import { type Command, type CommandContext } from '../command.js';
3
+ type CloudFile = V1Response<'/v1/files/{file_id}', 'get'>;
4
+ export interface FileUpload {
5
+ readonly body: globalThis.Blob;
6
+ readonly contentType: string;
7
+ readonly expiresAt: string | null;
8
+ readonly overwrite: boolean;
9
+ readonly path: string;
10
+ readonly sha256: string;
11
+ }
12
+ /**
13
+ * Uploads a file: in one request up to 80 MiB, otherwise in parts through an upload session,
14
+ * aborted when a part fails.
15
+ */
16
+ export declare function uploadFile(context: CommandContext, upload: FileUpload): Promise<CloudFile>;
17
+ /** A local file's SHA-256, hex, read as a stream. */
18
+ export declare function hashFile(localPath: string): Promise<string>;
19
+ /** The media type for a local file, by its extension. */
20
+ export declare function contentTypeFor(localPath: string): string;
21
+ export declare function formatBytes(bytes: number): string;
22
+ export declare const fileCommands: readonly Command[];
23
+ export {};
24
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../../src/cli/cloud/commands/files.ts"],"names":[],"mappings":"AAUA,OAAO,EAA4B,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,EASH,KAAK,OAAO,EACZ,KAAK,cAAc,EACtB,MAAM,eAAe,CAAC;AAIvB,KAAK,SAAS,GAAG,UAAU,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;AAgL1D,MAAM,WAAW,UAAU;IACvB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA4DhG;AA2BD,qDAAqD;AACrD,wBAAsB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIjE;AAwBD,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUjD;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,OAAO,EAA+C,CAAC"}
@@ -0,0 +1,284 @@
1
+ // `cloud files`: upload, download, share and delete the project's files.
2
+ import { CommandFailure } from '../../failure.js';
3
+ import { requireConsent } from '../../interaction.js';
4
+ import { createHash } from 'node:crypto';
5
+ import { createReadStream, createWriteStream, openAsBlob } from 'node:fs';
6
+ import { stat } from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import { Writable } from 'node:stream';
9
+ import { unwrap } from '@dexto/cloud-client';
10
+ import { argument, flag, integerOption, PAGE_OPTIONS, pageQuery, stringOption, UsageError, YES_OPTION, } from '../command.js';
11
+ import { sendV1, v1Failure } from '../v1.js';
12
+ /** Larger files go up in parts, through an upload session. */
13
+ const ONE_REQUEST_MAX_BYTES = 80 * 1024 * 1024;
14
+ const COLUMNS = [
15
+ { header: 'id', value: (file) => file.id },
16
+ { header: 'path', value: (file) => file.path },
17
+ { header: 'size', value: (file) => formatBytes(file.size_bytes) },
18
+ { header: 'type', value: (file) => file.content_type },
19
+ { header: 'updated', value: (file) => file.updated_at },
20
+ ];
21
+ const list = {
22
+ options: {
23
+ ...PAGE_OPTIONS,
24
+ contains: { description: 'Only paths containing this text.', type: 'string' },
25
+ prefix: { description: 'Only paths starting with this, such as reports/.', type: 'string' },
26
+ },
27
+ path: ['cloud', 'files', 'list'],
28
+ summary: "List the project's files.",
29
+ async run(context, input) {
30
+ const prefix = stringOption(input, 'prefix');
31
+ const contains = stringOption(input, 'contains');
32
+ const page = await unwrap(await (await context.client()).GET('/v1/files', {
33
+ params: {
34
+ query: {
35
+ ...pageQuery(input),
36
+ ...(prefix === undefined ? {} : { prefix }),
37
+ ...(contains === undefined ? {} : { contains }),
38
+ },
39
+ },
40
+ }));
41
+ context.output.list(page, COLUMNS);
42
+ },
43
+ };
44
+ const get = {
45
+ arguments: '<file>',
46
+ path: ['cloud', 'files', 'get'],
47
+ summary: 'Show a file by id or path.',
48
+ async run(context, input) {
49
+ const client = await context.client();
50
+ context.output.record(await getFile(client, await resolveFileId(client, argument(input, 0, 'file'))));
51
+ },
52
+ };
53
+ const upload = {
54
+ arguments: '<local_path> [path]',
55
+ options: {
56
+ 'content-type': {
57
+ description: 'The media type (default from the extension).',
58
+ type: 'string',
59
+ },
60
+ 'expires-at': {
61
+ description: 'Delete the file at this ISO 8601 time.',
62
+ type: 'string',
63
+ valueName: 'TIME',
64
+ },
65
+ overwrite: { description: 'Replace a file already at the path.', type: 'boolean' },
66
+ },
67
+ path: ['cloud', 'files', 'upload'],
68
+ summary: 'Upload a local file to a project path (default: its file name).',
69
+ async run(context, input) {
70
+ const localPath = argument(input, 0, 'local_path');
71
+ if (!(await stat(localPath).catch(() => null))?.isFile())
72
+ throw new UsageError(`${localPath} is not a file.`);
73
+ const body = await openAsBlob(localPath).catch((error) => {
74
+ throw new UsageError(`Cannot read ${localPath}: ${error instanceof Error ? error.message : 'unknown error'}.`);
75
+ });
76
+ const expiresAt = stringOption(input, 'expires-at');
77
+ const file = await uploadFile(context, {
78
+ body,
79
+ contentType: stringOption(input, 'content-type') ?? contentTypeFor(localPath),
80
+ expiresAt: expiresAt ?? null,
81
+ overwrite: flag(input, 'overwrite'),
82
+ path: input.positionals[1] ?? path.basename(localPath),
83
+ sha256: await hashFile(localPath),
84
+ });
85
+ context.output.result(file, `Uploaded ${file.path} (${formatBytes(file.size_bytes)}, ${file.id}).`);
86
+ },
87
+ };
88
+ const download = {
89
+ arguments: '<file> [local_path]',
90
+ path: ['cloud', 'files', 'download'],
91
+ summary: 'Download a file by id or path to a local path (default: its file name), or - for stdout.',
92
+ async run(context, input) {
93
+ const client = await context.client();
94
+ const fileId = await resolveFileId(client, argument(input, 0, 'file'));
95
+ const file = await getFile(client, fileId);
96
+ const target = input.positionals[1] ?? path.basename(file.path);
97
+ const { data, response } = await client.GET('/v1/files/{file_id}/content', {
98
+ params: { path: { file_id: fileId } },
99
+ parseAs: 'stream',
100
+ });
101
+ if (!response.ok || data === undefined || data === null)
102
+ throw v1Failure(context, response.status, await response.text().catch(() => ''));
103
+ if (target === '-') {
104
+ const reader = data.getReader();
105
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read())
106
+ await context.runtime.writeBytes(chunk.value);
107
+ return;
108
+ }
109
+ await data.pipeTo(Writable.toWeb(createWriteStream(target)));
110
+ context.output.result({ ...file, local_path: target }, `Saved ${file.path} to ${target}.`);
111
+ },
112
+ };
113
+ const url = {
114
+ arguments: '<file>',
115
+ options: {
116
+ 'expires-in': {
117
+ description: 'Seconds the URL works, up to 86400 (default 3600).',
118
+ type: 'string',
119
+ valueName: 'SECONDS',
120
+ },
121
+ },
122
+ path: ['cloud', 'files', 'url'],
123
+ summary: 'Create a URL anyone can read the file from until it expires.',
124
+ async run(context, input) {
125
+ const client = await context.client();
126
+ const expiresIn = integerOption(input, 'expires-in');
127
+ const result = await unwrap(await client.POST('/v1/files/{file_id}/url', {
128
+ params: {
129
+ path: { file_id: await resolveFileId(client, argument(input, 0, 'file')) },
130
+ },
131
+ body: expiresIn === undefined ? {} : { expires_in: expiresIn },
132
+ }));
133
+ context.output.result(result, result.url);
134
+ },
135
+ };
136
+ const remove = {
137
+ arguments: '<file>',
138
+ options: YES_OPTION,
139
+ path: ['cloud', 'files', 'delete'],
140
+ summary: 'Delete a file by id or path.',
141
+ async run(context, input) {
142
+ await requireConsent(context, flag(input, 'yes'), `Deleting ${argument(input, 0, 'file')} can't be undone.`);
143
+ const client = await context.client();
144
+ const result = await unwrap(await client.DELETE('/v1/files/{file_id}', {
145
+ params: {
146
+ path: { file_id: await resolveFileId(client, argument(input, 0, 'file')) },
147
+ },
148
+ }));
149
+ context.output.result(result, `Deleted file ${result.id}.`);
150
+ },
151
+ };
152
+ async function getFile(client, fileId) {
153
+ return await unwrap(await client.GET('/v1/files/{file_id}', { params: { path: { file_id: fileId } } }));
154
+ }
155
+ /**
156
+ * Uploads a file: in one request up to 80 MiB, otherwise in parts through an upload session,
157
+ * aborted when a part fails.
158
+ */
159
+ export async function uploadFile(context, upload) {
160
+ if (upload.body.size <= ONE_REQUEST_MAX_BYTES) {
161
+ const query = new URLSearchParams({ path: upload.path });
162
+ if (upload.overwrite)
163
+ query.set('overwrite', 'true');
164
+ if (upload.expiresAt !== null)
165
+ query.set('expires_at', upload.expiresAt);
166
+ const response = await sendV1(context, 'POST', `/v1/files?${query}`, {
167
+ body: upload.body,
168
+ headers: {
169
+ accept: 'application/json',
170
+ 'content-type': upload.contentType,
171
+ 'content-length': String(upload.body.size),
172
+ 'x-content-sha256': upload.sha256,
173
+ },
174
+ });
175
+ const text = await response.text();
176
+ if (!response.ok)
177
+ throw v1Failure(context, response.status, text);
178
+ return JSON.parse(text);
179
+ }
180
+ const client = await context.client();
181
+ const session = await unwrap(await client.POST('/v1/files/uploads', {
182
+ body: {
183
+ content_type: upload.contentType,
184
+ overwrite: upload.overwrite,
185
+ path: upload.path,
186
+ sha256: upload.sha256,
187
+ size_bytes: upload.body.size,
188
+ ...(upload.expiresAt === null ? {} : { expires_at: upload.expiresAt }),
189
+ },
190
+ }));
191
+ const params = { params: { path: { upload_id: session.id } } };
192
+ try {
193
+ if (session.upload_url === null)
194
+ throw new CommandFailure('failed', 'The upload session has no upload URL.', null);
195
+ // `parts` lists the parts already uploaded; send every other one of `part_count`.
196
+ const uploaded = new Set(session.parts.map((part) => part.part_number));
197
+ for (let partNumber = 1; partNumber <= session.part_count; partNumber += 1) {
198
+ if (uploaded.has(partNumber))
199
+ continue;
200
+ const start = (partNumber - 1) * session.part_size_bytes;
201
+ const response = await context.runtime.fetch(`${session.upload_url}/${partNumber}`, {
202
+ body: upload.body.slice(start, Math.min(start + session.part_size_bytes, session.size_bytes)),
203
+ method: 'PUT',
204
+ signal: context.runtime.signal,
205
+ });
206
+ if (!response.ok)
207
+ throw new CommandFailure('failed', `Part ${partNumber} of the upload was refused (HTTP ${response.status}).`, null);
208
+ }
209
+ return await unwrap(await client.POST('/v1/files/uploads/{upload_id}/complete', params));
210
+ }
211
+ catch (error) {
212
+ await client.POST('/v1/files/uploads/{upload_id}/abort', params).catch(() => undefined);
213
+ throw error;
214
+ }
215
+ }
216
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
217
+ /**
218
+ * A file id as given, or the id of the file at a path. The API addresses files only by id, so a
219
+ * path is found by listing its prefix.
220
+ */
221
+ async function resolveFileId(client, file) {
222
+ if (UUID.test(file))
223
+ return file;
224
+ let after;
225
+ for (;;) {
226
+ const page = await unwrap(await client.GET('/v1/files', {
227
+ params: {
228
+ query: { limit: 1000, prefix: file, ...(after === undefined ? {} : { after }) },
229
+ },
230
+ }));
231
+ const match = page.data.find((each) => each.path === file);
232
+ if (match !== undefined)
233
+ return match.id;
234
+ if (!page.has_more || page.next_after === null)
235
+ break;
236
+ after = page.next_after;
237
+ }
238
+ throw new CommandFailure('not_found', `No file at ${file}.`, null);
239
+ }
240
+ /** A local file's SHA-256, hex, read as a stream. */
241
+ export async function hashFile(localPath) {
242
+ const hash = createHash('sha256');
243
+ for await (const chunk of createReadStream(localPath))
244
+ hash.update(chunk);
245
+ return hash.digest('hex');
246
+ }
247
+ const CONTENT_TYPES = {
248
+ '.csv': 'text/csv',
249
+ '.gif': 'image/gif',
250
+ '.html': 'text/html',
251
+ '.jpeg': 'image/jpeg',
252
+ '.jpg': 'image/jpeg',
253
+ '.json': 'application/json',
254
+ '.m4v': 'video/mp4',
255
+ '.md': 'text/markdown',
256
+ '.mov': 'video/quicktime',
257
+ '.mp3': 'audio/mpeg',
258
+ '.mp4': 'video/mp4',
259
+ '.pdf': 'application/pdf',
260
+ '.png': 'image/png',
261
+ '.svg': 'image/svg+xml',
262
+ '.txt': 'text/plain',
263
+ '.wav': 'audio/wav',
264
+ '.webm': 'video/webm',
265
+ '.webp': 'image/webp',
266
+ '.zip': 'application/zip',
267
+ };
268
+ /** The media type for a local file, by its extension. */
269
+ export function contentTypeFor(localPath) {
270
+ return CONTENT_TYPES[path.extname(localPath).toLowerCase()] ?? 'application/octet-stream';
271
+ }
272
+ export function formatBytes(bytes) {
273
+ if (bytes < 1024)
274
+ return `${bytes} B`;
275
+ const units = ['KiB', 'MiB', 'GiB'];
276
+ let value = bytes / 1024;
277
+ let unit = 0;
278
+ while (value >= 1024 && unit < units.length - 1) {
279
+ value /= 1024;
280
+ unit += 1;
281
+ }
282
+ return `${value.toFixed(1)} ${units[unit]}`;
283
+ }
284
+ export const fileCommands = [list, get, upload, download, url, remove];
@@ -0,0 +1,3 @@
1
+ import { type Command } from '../command.js';
2
+ export declare const findCommands: readonly Command[];
3
+ //# sourceMappingURL=find.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../../../src/cli/cloud/commands/find.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2C,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAmFtF,eAAO,MAAM,YAAY,EAAE,SAAS,OAAO,EAAW,CAAC"}