dexto 1.14.4 → 2.0.0-beta.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.
Files changed (253) hide show
  1. package/dist/cli/auth/billing.d.ts +0 -6
  2. package/dist/cli/auth/billing.d.ts.map +1 -1
  3. package/dist/cli/auth/billing.js +0 -23
  4. package/dist/cli/auth/constants.d.ts +3 -31
  5. package/dist/cli/auth/constants.d.ts.map +1 -1
  6. package/dist/cli/auth/constants.js +9 -33
  7. package/dist/cli/auth/index.d.ts +2 -9
  8. package/dist/cli/auth/index.d.ts.map +1 -1
  9. package/dist/cli/auth/index.js +2 -7
  10. package/dist/cli/auth/sign-in-runtime.d.ts +12 -0
  11. package/dist/cli/auth/sign-in-runtime.d.ts.map +1 -0
  12. package/dist/cli/auth/sign-in-runtime.js +59 -0
  13. package/dist/cli/cloud/app-scopes.d.ts +4 -0
  14. package/dist/cli/cloud/app-scopes.d.ts.map +1 -0
  15. package/dist/cli/cloud/app-scopes.js +19 -0
  16. package/dist/cli/cloud/command.d.ts +143 -0
  17. package/dist/cli/cloud/command.d.ts.map +1 -0
  18. package/dist/cli/cloud/command.js +202 -0
  19. package/dist/cli/cloud/commands/account.d.ts +11 -0
  20. package/dist/cli/cloud/commands/account.d.ts.map +1 -0
  21. package/dist/cli/cloud/commands/account.js +128 -0
  22. package/dist/cli/cloud/commands/agents.d.ts +3 -0
  23. package/dist/cli/cloud/commands/agents.d.ts.map +1 -0
  24. package/dist/cli/cloud/commands/agents.js +524 -0
  25. package/dist/cli/cloud/commands/api.d.ts +3 -0
  26. package/dist/cli/cloud/commands/api.d.ts.map +1 -0
  27. package/dist/cli/cloud/commands/api.js +26 -0
  28. package/dist/cli/cloud/commands/app-files.d.ts +29 -0
  29. package/dist/cli/cloud/commands/app-files.d.ts.map +1 -0
  30. package/dist/cli/cloud/commands/app-files.js +72 -0
  31. package/dist/cli/cloud/commands/automations.d.ts +3 -0
  32. package/dist/cli/cloud/commands/automations.d.ts.map +1 -0
  33. package/dist/cli/cloud/commands/automations.js +232 -0
  34. package/dist/cli/cloud/commands/chats.d.ts +3 -0
  35. package/dist/cli/cloud/commands/chats.d.ts.map +1 -0
  36. package/dist/cli/cloud/commands/chats.js +189 -0
  37. package/dist/cli/cloud/commands/data-deletions.d.ts +3 -0
  38. package/dist/cli/cloud/commands/data-deletions.d.ts.map +1 -0
  39. package/dist/cli/cloud/commands/data-deletions.js +54 -0
  40. package/dist/cli/cloud/commands/deploys.d.ts +10 -0
  41. package/dist/cli/cloud/commands/deploys.d.ts.map +1 -0
  42. package/dist/cli/cloud/commands/deploys.js +223 -0
  43. package/dist/cli/cloud/commands/drive.d.ts +12 -0
  44. package/dist/cli/cloud/commands/drive.d.ts.map +1 -0
  45. package/dist/cli/cloud/commands/drive.js +320 -0
  46. package/dist/cli/cloud/commands/files.d.ts +24 -0
  47. package/dist/cli/cloud/commands/files.d.ts.map +1 -0
  48. package/dist/cli/cloud/commands/files.js +282 -0
  49. package/dist/cli/cloud/commands/find.d.ts +3 -0
  50. package/dist/cli/cloud/commands/find.d.ts.map +1 -0
  51. package/dist/cli/cloud/commands/find.js +63 -0
  52. package/dist/cli/cloud/commands/models.d.ts +3 -0
  53. package/dist/cli/cloud/commands/models.d.ts.map +1 -0
  54. package/dist/cli/cloud/commands/models.js +248 -0
  55. package/dist/cli/cloud/commands/profile.d.ts +3 -0
  56. package/dist/cli/cloud/commands/profile.d.ts.map +1 -0
  57. package/dist/cli/cloud/commands/profile.js +119 -0
  58. package/dist/cli/cloud/commands/projects.d.ts +5 -0
  59. package/dist/cli/cloud/commands/projects.d.ts.map +1 -0
  60. package/dist/cli/cloud/commands/projects.js +66 -0
  61. package/dist/cli/cloud/commands/publish.d.ts +3 -0
  62. package/dist/cli/cloud/commands/publish.d.ts.map +1 -0
  63. package/dist/cli/cloud/commands/publish.js +138 -0
  64. package/dist/cli/cloud/commands/sandboxes.d.ts +3 -0
  65. package/dist/cli/cloud/commands/sandboxes.d.ts.map +1 -0
  66. package/dist/cli/cloud/commands/sandboxes.js +265 -0
  67. package/dist/cli/cloud/commands/secrets.d.ts +3 -0
  68. package/dist/cli/cloud/commands/secrets.d.ts.map +1 -0
  69. package/dist/cli/cloud/commands/secrets.js +110 -0
  70. package/dist/cli/cloud/commands/skills.d.ts +3 -0
  71. package/dist/cli/cloud/commands/skills.d.ts.map +1 -0
  72. package/dist/cli/cloud/commands/skills.js +202 -0
  73. package/dist/cli/cloud/commands/web.d.ts +3 -0
  74. package/dist/cli/cloud/commands/web.d.ts.map +1 -0
  75. package/dist/cli/cloud/commands/web.js +53 -0
  76. package/dist/cli/cloud/commands/workspace-apps.d.ts +58 -0
  77. package/dist/cli/cloud/commands/workspace-apps.d.ts.map +1 -0
  78. package/dist/cli/cloud/commands/workspace-apps.js +656 -0
  79. package/dist/cli/cloud/commands/workspace.d.ts +36 -0
  80. package/dist/cli/cloud/commands/workspace.d.ts.map +1 -0
  81. package/dist/cli/cloud/commands/workspace.js +279 -0
  82. package/dist/cli/cloud/connection.d.ts +33 -0
  83. package/dist/cli/cloud/connection.d.ts.map +1 -0
  84. package/dist/cli/cloud/connection.js +38 -0
  85. package/dist/cli/cloud/events.d.ts +63 -0
  86. package/dist/cli/cloud/events.d.ts.map +1 -0
  87. package/dist/cli/cloud/events.js +72 -0
  88. package/dist/cli/cloud/host.d.ts +9 -0
  89. package/dist/cli/cloud/host.d.ts.map +1 -0
  90. package/dist/cli/cloud/host.js +105 -0
  91. package/dist/cli/cloud/local-chats.d.ts +73 -0
  92. package/dist/cli/cloud/local-chats.d.ts.map +1 -0
  93. package/dist/cli/cloud/local-chats.js +441 -0
  94. package/dist/cli/cloud/output.d.ts +34 -0
  95. package/dist/cli/cloud/output.d.ts.map +1 -0
  96. package/dist/cli/cloud/output.js +76 -0
  97. package/dist/cli/cloud/run.d.ts +26 -0
  98. package/dist/cli/cloud/run.d.ts.map +1 -0
  99. package/dist/cli/cloud/run.js +444 -0
  100. package/dist/cli/cloud/runtime.d.ts +33 -0
  101. package/dist/cli/cloud/runtime.d.ts.map +1 -0
  102. package/dist/cli/cloud/runtime.js +86 -0
  103. package/dist/cli/cloud/tar.d.ts +9 -0
  104. package/dist/cli/cloud/tar.d.ts.map +1 -0
  105. package/dist/cli/cloud/tar.js +69 -0
  106. package/dist/cli/cloud/transfer.d.ts +13 -0
  107. package/dist/cli/cloud/transfer.d.ts.map +1 -0
  108. package/dist/cli/cloud/transfer.js +35 -0
  109. package/dist/cli/cloud/v1.d.ts +19 -0
  110. package/dist/cli/cloud/v1.d.ts.map +1 -0
  111. package/dist/cli/cloud/v1.js +65 -0
  112. package/dist/cli/cloud/zip.d.ts +8 -0
  113. package/dist/cli/cloud/zip.d.ts.map +1 -0
  114. package/dist/cli/cloud/zip.js +66 -0
  115. package/dist/cli/commands/auth/sign-in-commands.d.ts +62 -0
  116. package/dist/cli/commands/auth/sign-in-commands.d.ts.map +1 -0
  117. package/dist/cli/commands/auth/sign-in-commands.js +247 -0
  118. package/dist/cli/commands/auth/sign-in-context.d.ts +16 -0
  119. package/dist/cli/commands/auth/sign-in-context.d.ts.map +1 -0
  120. package/dist/cli/commands/auth/sign-in-context.js +68 -0
  121. package/dist/cli/commands/cloud/register.d.ts +5 -0
  122. package/dist/cli/commands/cloud/register.d.ts.map +1 -1
  123. package/dist/cli/commands/cloud/register.js +42 -24
  124. package/dist/cli/commands/create-app.js +5 -6
  125. package/dist/cli/commands/create-image.d.ts.map +1 -1
  126. package/dist/cli/commands/create-image.js +9 -10
  127. package/dist/cli/commands/index.d.ts +0 -2
  128. package/dist/cli/commands/index.d.ts.map +1 -1
  129. package/dist/cli/commands/index.js +0 -2
  130. package/dist/cli/commands/init.d.ts +0 -2
  131. package/dist/cli/commands/init.d.ts.map +1 -1
  132. package/dist/cli/commands/init.js +5 -26
  133. package/dist/cli/commands/run/headless.js +2 -2
  134. package/dist/cli/commands/run/output.d.ts +29 -10
  135. package/dist/cli/commands/run/output.d.ts.map +1 -1
  136. package/dist/cli/commands/run/output.js +44 -22
  137. package/dist/cli/commands/run/register.d.ts.map +1 -1
  138. package/dist/cli/commands/run/register.js +21 -9
  139. package/dist/cli/commands/session/register.d.ts.map +1 -1
  140. package/dist/cli/commands/session/register.js +51 -21
  141. package/dist/cli/commands/session-commands.js +1 -1
  142. package/dist/cli/commands/setup.js +17 -14
  143. package/dist/cli/help-json.d.ts +22 -0
  144. package/dist/cli/help-json.d.ts.map +1 -0
  145. package/dist/cli/help-json.js +20 -0
  146. package/dist/cli/modes/cli.d.ts.map +1 -1
  147. package/dist/cli/modes/cli.js +42 -9
  148. package/dist/cli/modes/server.js +8 -8
  149. package/dist/cli/modes/web.js +3 -3
  150. package/dist/cli/utils/dexto-auth-check.d.ts +1 -1
  151. package/dist/cli/utils/dexto-auth-check.d.ts.map +1 -1
  152. package/dist/cli/utils/dexto-auth-check.js +4 -5
  153. package/dist/cli/utils/dexto-setup.d.ts +4 -5
  154. package/dist/cli/utils/dexto-setup.d.ts.map +1 -1
  155. package/dist/cli/utils/dexto-setup.js +17 -14
  156. package/dist/cli/utils/provider-setup.js +1 -1
  157. package/dist/cli/utils/scaffolding-utils.d.ts +9 -3
  158. package/dist/cli/utils/scaffolding-utils.d.ts.map +1 -1
  159. package/dist/cli/utils/scaffolding-utils.js +48 -40
  160. package/dist/cli/utils/version-check.d.ts.map +1 -1
  161. package/dist/cli/utils/version-check.js +2 -1
  162. package/dist/index-main.js +33 -65
  163. package/package.json +8 -7
  164. package/dist/cli/auth/api-client.d.ts +0 -125
  165. package/dist/cli/auth/api-client.d.ts.map +0 -1
  166. package/dist/cli/auth/api-client.js +0 -585
  167. package/dist/cli/auth/device.d.ts +0 -14
  168. package/dist/cli/auth/device.d.ts.map +0 -1
  169. package/dist/cli/auth/device.js +0 -84
  170. package/dist/cli/auth/dexto-api-key.d.ts +0 -34
  171. package/dist/cli/auth/dexto-api-key.d.ts.map +0 -1
  172. package/dist/cli/auth/dexto-api-key.js +0 -160
  173. package/dist/cli/auth/login-persistence.d.ts +0 -15
  174. package/dist/cli/auth/login-persistence.d.ts.map +0 -1
  175. package/dist/cli/auth/login-persistence.js +0 -72
  176. package/dist/cli/auth/oauth.d.ts +0 -39
  177. package/dist/cli/auth/oauth.d.ts.map +0 -1
  178. package/dist/cli/auth/oauth.js +0 -434
  179. package/dist/cli/auth/service.d.ts +0 -24
  180. package/dist/cli/auth/service.d.ts.map +0 -1
  181. package/dist/cli/auth/service.js +0 -161
  182. package/dist/cli/auth/types.d.ts +0 -11
  183. package/dist/cli/auth/types.d.ts.map +0 -1
  184. package/dist/cli/auth/types.js +0 -1
  185. package/dist/cli/cloud-chat.d.ts +0 -18
  186. package/dist/cli/cloud-chat.d.ts.map +0 -1
  187. package/dist/cli/cloud-chat.js +0 -734
  188. package/dist/cli/commands/auth/index.d.ts +0 -4
  189. package/dist/cli/commands/auth/index.d.ts.map +0 -1
  190. package/dist/cli/commands/auth/index.js +0 -4
  191. package/dist/cli/commands/auth/login.d.ts +0 -12
  192. package/dist/cli/commands/auth/login.d.ts.map +0 -1
  193. package/dist/cli/commands/auth/login.js +0 -153
  194. package/dist/cli/commands/auth/logout.d.ts +0 -5
  195. package/dist/cli/commands/auth/logout.d.ts.map +0 -1
  196. package/dist/cli/commands/auth/logout.js +0 -81
  197. package/dist/cli/commands/auth/register.d.ts +0 -3
  198. package/dist/cli/commands/auth/register.d.ts.map +0 -1
  199. package/dist/cli/commands/auth/register.js +0 -96
  200. package/dist/cli/commands/auth/status.d.ts +0 -2
  201. package/dist/cli/commands/auth/status.d.ts.map +0 -1
  202. package/dist/cli/commands/auth/status.js +0 -22
  203. package/dist/cli/commands/billing/index.d.ts +0 -2
  204. package/dist/cli/commands/billing/index.d.ts.map +0 -1
  205. package/dist/cli/commands/billing/index.js +0 -2
  206. package/dist/cli/commands/billing/register.d.ts +0 -3
  207. package/dist/cli/commands/billing/register.d.ts.map +0 -1
  208. package/dist/cli/commands/billing/register.js +0 -20
  209. package/dist/cli/commands/billing/status.d.ts +0 -8
  210. package/dist/cli/commands/billing/status.d.ts.map +0 -1
  211. package/dist/cli/commands/billing/status.js +0 -71
  212. package/dist/cli/commands/cloud/discovery.d.ts +0 -17
  213. package/dist/cli/commands/cloud/discovery.d.ts.map +0 -1
  214. package/dist/cli/commands/cloud/discovery.js +0 -88
  215. package/dist/cli/commands/deploy/client.d.ts +0 -113
  216. package/dist/cli/commands/deploy/client.d.ts.map +0 -1
  217. package/dist/cli/commands/deploy/client.js +0 -519
  218. package/dist/cli/commands/deploy/config.d.ts +0 -37
  219. package/dist/cli/commands/deploy/config.d.ts.map +0 -1
  220. package/dist/cli/commands/deploy/config.js +0 -144
  221. package/dist/cli/commands/deploy/entry-agent.d.ts +0 -3
  222. package/dist/cli/commands/deploy/entry-agent.d.ts.map +0 -1
  223. package/dist/cli/commands/deploy/entry-agent.js +0 -70
  224. package/dist/cli/commands/deploy/index.d.ts +0 -12
  225. package/dist/cli/commands/deploy/index.d.ts.map +0 -1
  226. package/dist/cli/commands/deploy/index.js +0 -370
  227. package/dist/cli/commands/deploy/links.d.ts +0 -3
  228. package/dist/cli/commands/deploy/links.d.ts.map +0 -1
  229. package/dist/cli/commands/deploy/links.js +0 -53
  230. package/dist/cli/commands/deploy/register.d.ts +0 -6
  231. package/dist/cli/commands/deploy/register.d.ts.map +0 -1
  232. package/dist/cli/commands/deploy/register.js +0 -128
  233. package/dist/cli/commands/deploy/snapshot.d.ts +0 -12
  234. package/dist/cli/commands/deploy/snapshot.d.ts.map +0 -1
  235. package/dist/cli/commands/deploy/snapshot.js +0 -76
  236. package/dist/cli/commands/deploy/state.d.ts +0 -13
  237. package/dist/cli/commands/deploy/state.d.ts.map +0 -1
  238. package/dist/cli/commands/deploy/state.js +0 -122
  239. package/dist/cli/commands/span/register.d.ts +0 -6
  240. package/dist/cli/commands/span/register.d.ts.map +0 -1
  241. package/dist/cli/commands/span/register.js +0 -25
  242. package/dist/cli/commands/trace/client.d.ts +0 -98
  243. package/dist/cli/commands/trace/client.d.ts.map +0 -1
  244. package/dist/cli/commands/trace/client.js +0 -150
  245. package/dist/cli/commands/trace/format.d.ts +0 -5
  246. package/dist/cli/commands/trace/format.d.ts.map +0 -1
  247. package/dist/cli/commands/trace/format.js +0 -58
  248. package/dist/cli/commands/trace/index.d.ts +0 -21
  249. package/dist/cli/commands/trace/index.d.ts.map +0 -1
  250. package/dist/cli/commands/trace/index.js +0 -49
  251. package/dist/cli/commands/trace/register.d.ts +0 -6
  252. package/dist/cli/commands/trace/register.d.ts.map +0 -1
  253. package/dist/cli/commands/trace/register.js +0 -71
@@ -0,0 +1,223 @@
1
+ // `cloud sites` and `cloud functions`: pack a project with its own packer, deploy, list and delete.
2
+ import { access, mkdtemp, readFile, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { z } from 'zod';
6
+ import { argument, CommandFailure, flag, MissingScopeFailure, requireConsent, stringOption, UsageError, YES_OPTION, } from '../command.js';
7
+ import { callV1 } from '../v1.js';
8
+ const PACKER = path.join('.dexto', 'pack.mjs');
9
+ const PROVIDERS = ['cloudflare-workers', 'aws-lambda'];
10
+ const SiteSchema = z.object({
11
+ id: z.string(),
12
+ slug: z.string(),
13
+ url: z.string(),
14
+ visibility: z.string(),
15
+ });
16
+ const FunctionSchema = z.object({
17
+ access: z.string(),
18
+ id: z.string(),
19
+ slug: z.string(),
20
+ status: z.string(),
21
+ url: z.string().nullable(),
22
+ });
23
+ const DeployedSiteSchema = z.object({
24
+ deployment: z.object({ id: z.string() }),
25
+ promoted: z.boolean(),
26
+ site: z.object({ id: z.string(), slug: z.string() }),
27
+ url: z.string(),
28
+ });
29
+ const DeployedFunctionSchema = z.object({
30
+ deployment: z.object({ id: z.string(), preview_url: z.string() }),
31
+ dexto: z.object({ configured: z.boolean(), scopes: z.array(z.string()) }).nullable(),
32
+ function: z.object({ id: z.string(), slug: z.string() }),
33
+ promoted: z.boolean(),
34
+ url: z.string().nullable(),
35
+ });
36
+ const DEPLOY_OPTIONS = {
37
+ 'no-promote': {
38
+ description: 'Deploy a preview to promote later, instead of making it live.',
39
+ type: 'boolean',
40
+ },
41
+ provider: {
42
+ description: `Where its Function runs: ${PROVIDERS.join(' or ')}. Omit for the default.`,
43
+ type: 'string',
44
+ valueName: 'PROVIDER',
45
+ },
46
+ };
47
+ const deploySite = {
48
+ arguments: '[dir]',
49
+ options: DEPLOY_OPTIONS,
50
+ path: ['cloud', 'sites', 'deploy'],
51
+ summary: 'Pack the Site in dir (default: here) with its packer and deploy it to the Cloud project; it goes live unless --no-promote.',
52
+ async run(context, input) {
53
+ await requireLoginScopes(context, ['sites:write', 'cloud:write']);
54
+ // Options are checked before packing, which can build and rewrite project files.
55
+ const query = deployQuery(input, {});
56
+ const packed = await pack(context, input.positionals[0], 'site');
57
+ const deployed = DeployedSiteSchema.parse(await callV1(context, 'POST', `/v1/sites/deployments?${query}`, packed));
58
+ context.output.result(deployed, deployed.promoted
59
+ ? `Deployed ${deployed.site.slug} to ${deployed.url} (deployment ${deployed.deployment.id}).`
60
+ : `Deployed a preview of ${deployed.site.slug} at ${deployed.url}. Make it live with: dexto cloud api POST /v1/sites/${deployed.site.id}/deployments/${deployed.deployment.id}/promote`);
61
+ },
62
+ };
63
+ const deployFunction = {
64
+ arguments: '[dir]',
65
+ options: {
66
+ ...DEPLOY_OPTIONS,
67
+ public: {
68
+ description: 'Let anyone on the internet reach it. Functions are private by default.',
69
+ type: 'boolean',
70
+ },
71
+ },
72
+ path: ['cloud', 'functions', 'deploy'],
73
+ summary: 'Pack the Function in dir (default: here) with its packer and deploy it to the Cloud project; it goes live unless --no-promote.',
74
+ async run(context, input) {
75
+ await requireLoginScopes(context, ['functions:write', 'cloud:write']);
76
+ // Options are checked before packing, which can build and rewrite project files.
77
+ const query = deployQuery(input, {
78
+ access: flag(input, 'public') ? 'public' : 'private',
79
+ });
80
+ const packed = await pack(context, input.positionals[0], 'function');
81
+ const deployed = DeployedFunctionSchema.parse(await callV1(context, 'POST', `/v1/functions/deployments?${query}`, packed));
82
+ const key = deployed.dexto === null || deployed.dexto.configured
83
+ ? ''
84
+ : ` Its Dexto key was not bound: your sign-in lacks ${deployed.dexto.scopes.join(', ')}.`;
85
+ context.output.result(deployed, deployed.promoted
86
+ ? `Deployed ${deployed.function.slug}${deployed.url === null ? '' : ` to ${deployed.url}`} (deployment ${deployed.deployment.id}).${key}`
87
+ : `Deployed ${deployed.function.slug} without making it live; preview at ${deployed.deployment.preview_url}. Make it live with: dexto cloud api POST /v1/functions/${deployed.function.id}/deployments/${deployed.deployment.id}/promote${key}`);
88
+ },
89
+ };
90
+ const SITE_COLUMNS = [
91
+ { header: 'id', value: (site) => site.id },
92
+ { header: 'slug', value: (site) => site.slug },
93
+ { header: 'visibility', value: (site) => site.visibility },
94
+ { header: 'url', value: (site) => site.url },
95
+ ];
96
+ const FUNCTION_COLUMNS = [
97
+ { header: 'id', value: (deployed) => deployed.id },
98
+ { header: 'slug', value: (deployed) => deployed.slug },
99
+ { header: 'status', value: (deployed) => deployed.status },
100
+ { header: 'access', value: (deployed) => deployed.access },
101
+ { header: 'url', value: (deployed) => deployed.url },
102
+ ];
103
+ const listSites = {
104
+ path: ['cloud', 'sites', 'list'],
105
+ summary: "List the Cloud project's Sites.",
106
+ async run(context) {
107
+ await requireLoginScopes(context, ['sites:read']);
108
+ const page = z
109
+ .object({ data: z.array(SiteSchema) })
110
+ .parse(await callV1(context, 'GET', '/v1/sites', null));
111
+ context.output.list(page, SITE_COLUMNS);
112
+ },
113
+ };
114
+ const deleteSite = {
115
+ arguments: '<site>',
116
+ options: YES_OPTION,
117
+ path: ['cloud', 'sites', 'delete'],
118
+ summary: 'Delete a Site of the Cloud project, by id or slug, with its deployments and data; its slug is free again. Asks first.',
119
+ async run(context, input) {
120
+ // The Site is found by listing, which takes sites:read, before it is deleted.
121
+ await requireLoginScopes(context, ['sites:read', 'sites:write', 'cloud:write']);
122
+ const wanted = argument(input, 0, 'site');
123
+ const sites = z
124
+ .object({ data: z.array(SiteSchema) })
125
+ .parse(await callV1(context, 'GET', '/v1/sites', null)).data;
126
+ const site = sites.find((each) => each.id === wanted || each.slug === wanted);
127
+ if (site === undefined)
128
+ throw new CommandFailure('not_found', `The Cloud project has no Site ${wanted}.`, 'dexto cloud sites list');
129
+ await requireConsent(context, flag(input, 'yes'), `This deletes ${site.slug} (${site.url}) with its deployments and data. It can't be undone.`);
130
+ await callV1(context, 'DELETE', `/v1/sites/${encodeURIComponent(site.id)}`, null);
131
+ context.output.result({ deleted: true, id: site.id, slug: site.slug }, `Deleted ${site.slug}. Its slug is free to deploy again.`);
132
+ },
133
+ };
134
+ const listFunctions = {
135
+ path: ['cloud', 'functions', 'list'],
136
+ summary: "List the Cloud project's Functions.",
137
+ async run(context) {
138
+ await requireLoginScopes(context, ['functions:read']);
139
+ const page = z
140
+ .object({ data: z.array(FunctionSchema) })
141
+ .parse(await callV1(context, 'GET', '/v1/functions', null));
142
+ context.output.list(page, FUNCTION_COLUMNS);
143
+ },
144
+ };
145
+ /**
146
+ * A person's sign-in needs the kind's permission, and to deploy also `cloud:write`, which the API
147
+ * checks first; asking for every missing one at once lets a single sign-in cover the command. A
148
+ * deploy spends credits, so running one is asking for `cloud:write`. At a terminal the CLI then
149
+ * offers to sign in again for them. A key's permissions are checked by the API.
150
+ */
151
+ export async function requireLoginScopes(context, scopes) {
152
+ const connection = await context.connection();
153
+ if (connection.kind !== 'oauth')
154
+ return;
155
+ const missing = scopes.filter((scope) => !connection.login.scopes.includes(scope));
156
+ if (missing.length === 0)
157
+ return;
158
+ throw new MissingScopeFailure(context, missing.join(','), missing.length === 1
159
+ ? `This needs the ${missing[0]} permission, which your sign-in lacks.`
160
+ : `This needs the ${missing.join(' and ')} permissions, which your sign-in lacks.`);
161
+ }
162
+ function deployQuery(input, extra) {
163
+ const provider = stringOption(input, 'provider');
164
+ if (provider !== undefined && !PROVIDERS.some((each) => each === provider))
165
+ throw new UsageError(`--provider must be ${PROVIDERS.join(' or ')}.`);
166
+ return new URLSearchParams({
167
+ ...extra,
168
+ promote: flag(input, 'no-promote') ? 'false' : 'true',
169
+ ...(provider === undefined ? {} : { provider }),
170
+ });
171
+ }
172
+ /**
173
+ * Runs the project's own packer, `.dexto/pack.mjs`, or its `pack` script when it has none, into a
174
+ * temporary file, and returns the deploy request it wrote once it is the kind asked for.
175
+ */
176
+ async function pack(context, dir, kind) {
177
+ const projectDir = path.resolve(dir ?? '.');
178
+ const outputDir = await mkdtemp(path.join(tmpdir(), 'dexto-pack-'));
179
+ const outputPath = path.join(outputDir, 'deploy-request.json');
180
+ try {
181
+ const ownPacker = await exists(path.join(projectDir, PACKER));
182
+ const [command, args] = ownPacker
183
+ ? ['node', [PACKER, '.', '--out', outputPath]]
184
+ : ['pnpm', ['run', 'pack', '.', '--out', outputPath]];
185
+ context.output.note(`Packing ${projectDir} with ${ownPacker ? PACKER : 'pnpm run pack'}…`);
186
+ const packed = await context.runtime.runProcess(command, args, { cwd: projectDir });
187
+ if (packed.exitCode !== 0)
188
+ throw new CommandFailure('invalid_input', `Packing failed:\n${packed.output.trim()}`, null);
189
+ const text = await readFile(outputPath, 'utf8').catch(() => {
190
+ throw new CommandFailure('failed', `The packer wrote no deploy request to ${outputPath}.`, null);
191
+ });
192
+ const packedKind = packageKind(text);
193
+ if (packedKind !== kind)
194
+ throw new UsageError(`${projectDir} packs a ${packedKind}. Deploy it with dexto cloud ${packedKind}s deploy.`);
195
+ return text;
196
+ }
197
+ finally {
198
+ await rm(outputDir, { force: true, recursive: true });
199
+ }
200
+ }
201
+ /** A Site's deploy request has pages (`assets`); a Function's has none. */
202
+ function packageKind(text) {
203
+ const parsed = JSON.parse(text);
204
+ return typeof parsed === 'object' && parsed !== null && 'assets' in parsed
205
+ ? 'site'
206
+ : 'function';
207
+ }
208
+ async function exists(file) {
209
+ try {
210
+ await access(file);
211
+ return true;
212
+ }
213
+ catch {
214
+ return false;
215
+ }
216
+ }
217
+ export const deployCommands = [
218
+ deploySite,
219
+ listSites,
220
+ deleteSite,
221
+ deployFunction,
222
+ listFunctions,
223
+ ];
@@ -0,0 +1,12 @@
1
+ import { type Command, type CommandContext } from '../command.js';
2
+ /**
3
+ * Uploads one local file into an app's own Drive folder, which gives the app the file to refer to
4
+ * as `drive:<id>`.
5
+ */
6
+ export declare function putAppFile(context: CommandContext, localPath: string, appId: string): Promise<{
7
+ id: string;
8
+ mimeType: string;
9
+ name: string;
10
+ }>;
11
+ export declare const driveCommands: readonly Command[];
12
+ //# sourceMappingURL=drive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drive.d.ts","sourceRoot":"","sources":["../../../../src/cli/cloud/commands/drive.ts"],"names":[],"mappings":"AASA,OAAO,EAKH,KAAK,OAAO,EACZ,KAAK,cAAc,EACtB,MAAM,eAAe,CAAC;AA4VvB;;;GAGG;AACH,wBAAsB,UAAU,CAC5B,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAezD;AAwED,eAAO,MAAM,aAAa,EAAE,SAAS,OAAO,EAAU,CAAC"}
@@ -0,0 +1,320 @@
1
+ // `drive put`: uploads local files and folder trees to Drive, versioning same-named files.
2
+ import { randomUUID } from 'node:crypto';
3
+ import { openAsBlob } from 'node:fs';
4
+ import { lstat, readdir, stat } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { z } from 'zod';
7
+ import { CommandFailure, isJsonObject, stringOption, UsageError, } from '../command.js';
8
+ import { sendBytes } from '../transfer.js';
9
+ import { contentTypeFor, formatBytes, hashFile } from './files.js';
10
+ import { queryWorkspaceTool } from './workspace.js';
11
+ /** How many files upload at once. */
12
+ const CONCURRENCY = 4;
13
+ /** How many times a part, or the completion, is sent before a network failure gives it up. */
14
+ const PART_ATTEMPTS = 3;
15
+ /** How long giving an upload up may take, even after the command was cancelled. */
16
+ const ABORT_TIMEOUT_MS = 10_000;
17
+ /** A Drive item as a list describes it. */
18
+ const ItemSchema = z
19
+ .object({
20
+ id: z.string(),
21
+ kind: z.string(),
22
+ name: z.string(),
23
+ parentId: z.string().nullable(),
24
+ revision: z.number(),
25
+ sha256: z.string().nullable().default(null),
26
+ size: z.number().nullable().default(null),
27
+ trashedAt: z.string().nullable().default(null),
28
+ })
29
+ .passthrough();
30
+ const UsageSchema = z.object({ limits: z.object({ fileBytes: z.number() }) });
31
+ const PageSchema = z.object({ items: z.array(ItemSchema), nextOffset: z.number().nullable() });
32
+ const FolderSchema = z.object({ item: z.object({ id: z.string() }).passthrough() });
33
+ const Endpoint = z.object({ method: z.string(), url: z.string() });
34
+ /** Where a reserved upload goes: one URL, or, for a large file, its parts and their completion. */
35
+ const ReservedSchema = z.object({
36
+ upload: z.union([
37
+ z.object({
38
+ multipart: z.object({
39
+ abort: Endpoint,
40
+ complete: Endpoint,
41
+ partCount: z.number().int().positive(),
42
+ partSize: z.number().int().positive(),
43
+ parts: Endpoint,
44
+ }),
45
+ }),
46
+ Endpoint,
47
+ ]),
48
+ });
49
+ const put = {
50
+ arguments: '<paths...>',
51
+ options: {
52
+ to: {
53
+ description: 'The Drive folder to put them in, by id (default: the top of your Drive).',
54
+ type: 'string',
55
+ valueName: 'FOLDER_ID',
56
+ },
57
+ },
58
+ path: ['workspace', 'drive', 'put'],
59
+ 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.',
60
+ async run(context, input) {
61
+ const to = stringOption(input, 'to') ?? null;
62
+ const { files, folders } = await collect(input.positionals);
63
+ const usage = UsageSchema.parse(await queryWorkspaceTool(context, 'dexto_drive', { action: 'usage' }));
64
+ const limit = usage.limits.fileBytes;
65
+ const tooLarge = files.filter((file) => file.size > limit);
66
+ if (tooLarge.length > 0)
67
+ throw new CommandFailure('invalid_input', `Drive takes files up to ${formatBytes(limit)}, and ${tooLarge
68
+ .map((file) => `${file.localPath} is ${formatBytes(file.size)}`)
69
+ .join(', ')}. Leave ${tooLarge.length === 1 ? 'it' : 'them'} out and run it again.`, null);
70
+ // Listing the destination first fails the run at once when it isn't a folder you can use.
71
+ const listed = new Map();
72
+ await listFolder(context, listed, to);
73
+ // Parents come before their children, so each folder's parent is there by the time it is.
74
+ const folderIds = new Map();
75
+ const putFolders = [];
76
+ for (const folder of folders) {
77
+ const parentId = folder.parent === null ? to : (folderIds.get(folder.parent) ?? null);
78
+ const name = path.basename(path.resolve(folder.localPath));
79
+ const existing = await findItem(context, listed, parentId, 'folder', name);
80
+ const item = existing ??
81
+ FolderSchema.parse(await queryWorkspaceTool(context, 'dexto_drive', {
82
+ action: 'folder',
83
+ name,
84
+ parentId,
85
+ requestId: randomUUID(),
86
+ })).item;
87
+ // A folder made just now has nothing in it to list.
88
+ if (existing === null)
89
+ listed.set(item.id, Promise.resolve([]));
90
+ folderIds.set(folder.localPath, item.id);
91
+ putFolders.push({
92
+ item,
93
+ path: folder.localPath,
94
+ status: existing === null ? 'created' : 'existing',
95
+ });
96
+ }
97
+ const saved = [];
98
+ const failed = [];
99
+ // Files bound for one name in one folder go one after another, each a version of the last.
100
+ const bound = new Map();
101
+ for (const file of files) {
102
+ const parentId = file.folder === null ? to : (folderIds.get(file.folder) ?? null);
103
+ const key = `${parentId ?? ''}/${path.basename(file.localPath)}`;
104
+ bound.set(key, [...(bound.get(key) ?? []), file]);
105
+ }
106
+ await eachAtOnce([...bound.values()], async (sameName) => {
107
+ for (const file of sameName) {
108
+ const parentId = file.folder === null ? to : (folderIds.get(file.folder) ?? null);
109
+ try {
110
+ const existing = await findItem(context, listed, parentId, 'file', path.basename(file.localPath));
111
+ const sha256 = await hashFile(file.localPath);
112
+ // Drive already has these bytes under this name: nothing to send.
113
+ if (existing?.sha256 === sha256 && existing.size === file.size) {
114
+ saved.push({ item: existing, path: file.localPath, status: 'unchanged' });
115
+ if (!context.output.json)
116
+ context.runtime.writeOutput(`Unchanged ${file.localPath} (${formatBytes(file.size)}, ${existing.id})\n`);
117
+ continue;
118
+ }
119
+ const item = await putFile(context, file, parentId, existing, sha256, null);
120
+ await remember(listed, parentId, item);
121
+ const status = existing === null ? 'created' : 'updated';
122
+ saved.push({ item, path: file.localPath, status });
123
+ if (!context.output.json)
124
+ context.runtime.writeOutput(`${status === 'created' ? 'Created' : 'Updated'} ${file.localPath} (${formatBytes(file.size)}, ${String(item.id)})\n`);
125
+ }
126
+ catch (error) {
127
+ // A sign-in that can't write fails every file the same way: stop, say so once.
128
+ if (error instanceof CommandFailure &&
129
+ (error.kind === 'needs_sign_in' || error.kind === 'needs_permission'))
130
+ throw error;
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ failed.push({ error: message, path: file.localPath });
133
+ context.runtime.writeError(`Couldn't upload ${file.localPath}: ${message}\n`);
134
+ }
135
+ }
136
+ });
137
+ const created = saved.filter((each) => each.status === 'created').length;
138
+ const updated = saved.filter((each) => each.status === 'updated').length;
139
+ const unchanged = saved.length - created - updated;
140
+ const madeFolders = putFolders.filter((each) => each.status === 'created').length;
141
+ context.output.result({
142
+ created,
143
+ failed,
144
+ files: saved,
145
+ folders: putFolders,
146
+ unchanged,
147
+ updated,
148
+ }, `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
149
+ ? ''
150
+ : ` Folders: ${madeFolders} created, ${putFolders.length - madeFolders} already there.`}`);
151
+ if (failed.length > 0)
152
+ throw new CommandFailure('failed', `${failed.length} file${failed.length === 1 ? '' : 's'} didn't upload: ${failed.map((each) => each.path).join(', ')}.`, null);
153
+ },
154
+ };
155
+ /**
156
+ * The files and folders the paths name. A folder brings what's in it, except names starting with a
157
+ * dot and links, which are left out; a path named on its own is taken as it is.
158
+ */
159
+ async function collect(paths) {
160
+ const files = [];
161
+ const folders = [];
162
+ const visit = async (localPath, parent, named) => {
163
+ const stats = await (named ? stat(localPath) : lstat(localPath)).catch(() => null);
164
+ if (stats?.isFile() === true) {
165
+ files.push({ folder: parent, localPath, size: stats.size });
166
+ return;
167
+ }
168
+ if (stats?.isDirectory() !== true) {
169
+ if (named)
170
+ throw new UsageError(`${localPath} is not a file or folder.`);
171
+ return;
172
+ }
173
+ folders.push({ localPath, parent });
174
+ const names = (await readdir(localPath)).filter((name) => !name.startsWith('.')).sort();
175
+ for (const name of names)
176
+ await visit(path.join(localPath, name), localPath, false);
177
+ };
178
+ for (const localPath of paths)
179
+ await visit(localPath, null, true);
180
+ return { files, folders };
181
+ }
182
+ /** The item of this kind and name in a Drive folder (null for the top), listing it once a run. */
183
+ async function findItem(context, listed, parentId, kind, name) {
184
+ const items = await listFolder(context, listed, parentId);
185
+ return items.find((item) => item.kind === kind && item.name === name) ?? null;
186
+ }
187
+ /** Puts what an upload saved in its folder's listing, so the next file of that name versions it. */
188
+ async function remember(listed, parentId, saved) {
189
+ const item = ItemSchema.safeParse(saved);
190
+ if (!item.success)
191
+ return;
192
+ const key = parentId ?? '';
193
+ const items = (await listed.get(key)) ?? [];
194
+ listed.set(key, Promise.resolve([...items.filter((each) => each.id !== item.data.id), item.data]));
195
+ }
196
+ /** Every item in a Drive folder, page by page, trash left out; each folder is listed once. */
197
+ function listFolder(context, listed, parentId) {
198
+ const known = listed.get(parentId ?? '');
199
+ if (known !== undefined)
200
+ return known;
201
+ const listing = (async () => {
202
+ const items = [];
203
+ for (let offset = 0; offset !== null;) {
204
+ const page = PageSchema.parse(await queryWorkspaceTool(context, 'dexto_drive', {
205
+ action: 'list',
206
+ offset,
207
+ parentId,
208
+ }));
209
+ items.push(...page.items.filter((item) => item.trashedAt === null));
210
+ offset = page.nextOffset;
211
+ }
212
+ return items;
213
+ })();
214
+ listed.set(parentId ?? '', listing);
215
+ return listing;
216
+ }
217
+ /**
218
+ * Reserves one file in Drive under a new request id, as a new version of `existing` when there is
219
+ * one, then sends its bytes to the URL Drive returns. A file too large for one request would be
220
+ * sent in parts here, once Drive takes them.
221
+ */
222
+ async function putFile(context, file, parentId, existing, sha256,
223
+ /** The app whose own folder the file goes into instead, which gives the app the file. */
224
+ appId) {
225
+ const mimeType = contentTypeFor(file.localPath);
226
+ const reserved = ReservedSchema.safeParse(await queryWorkspaceTool(context, 'dexto_drive', {
227
+ action: 'upload',
228
+ ...(appId === null ? {} : { appId }),
229
+ mimeType,
230
+ name: path.basename(file.localPath),
231
+ requestId: randomUUID(),
232
+ sha256,
233
+ size: file.size,
234
+ ...(existing === null
235
+ ? { parentId }
236
+ : {
237
+ expectedRevision: existing.revision,
238
+ parentId: existing.parentId,
239
+ targetId: existing.id,
240
+ }),
241
+ }));
242
+ if (!reserved.success)
243
+ throw new CommandFailure('failed', "Drive didn't say where to send the file.", null);
244
+ const { upload } = reserved.data;
245
+ const bytes = await openAsBlob(file.localPath);
246
+ const reply = 'multipart' in upload
247
+ ? await sendParts(context, upload.multipart, bytes)
248
+ : await sendBytes(context, { headers: { 'content-type': mimeType }, method: upload.method, url: upload.url }, bytes);
249
+ return isJsonObject(reply) && isJsonObject(reply.item) ? reply.item : {};
250
+ }
251
+ /**
252
+ * Uploads one local file into an app's own Drive folder, which gives the app the file to refer to
253
+ * as `drive:<id>`.
254
+ */
255
+ export async function putAppFile(context, localPath, appId) {
256
+ const info = await stat(localPath).catch(() => null);
257
+ if (info === null || !info.isFile())
258
+ throw new UsageError(`Can't read ${localPath}: give a file's path, or a link.`);
259
+ const item = await putFile(context, { folder: null, localPath, size: info.size }, null, null, await hashFile(localPath), appId);
260
+ if (typeof item.id !== 'string')
261
+ throw new CommandFailure('failed', `Drive didn't return ${localPath}'s file.`, null);
262
+ return { id: item.id, mimeType: contentTypeFor(localPath), name: path.basename(localPath) };
263
+ }
264
+ /**
265
+ * Sends a large file in the exact parts Drive asked for, a few at once, then completes it. A part
266
+ * or completion the network drops is sent again; one that still fails, or is refused, gives the
267
+ * upload up at once, so it holds no quota until it expires, even when the command was cancelled.
268
+ */
269
+ async function sendParts(context, plan, bytes) {
270
+ const numbers = Array.from({ length: plan.partCount }, (_, index) => index + 1);
271
+ try {
272
+ await eachAtOnce(numbers, async (number) => {
273
+ await sendRetrying(context, { headers: {}, method: plan.parts.method, url: `${plan.parts.url}/${number}` }, bytes.slice((number - 1) * plan.partSize, number * plan.partSize), 1);
274
+ });
275
+ return await sendRetrying(context, { headers: {}, method: plan.complete.method, url: plan.complete.url }, new globalThis.Blob([]), 1);
276
+ }
277
+ catch (error) {
278
+ // Its own signal: the command's is already aborted when Ctrl-C ended the upload.
279
+ await context.runtime
280
+ .fetch(plan.abort.url, {
281
+ method: plan.abort.method,
282
+ signal: AbortSignal.timeout(ABORT_TIMEOUT_MS),
283
+ })
284
+ .catch(() => undefined);
285
+ throw error;
286
+ }
287
+ }
288
+ /**
289
+ * Sends a part or the completion, again after a second and then two when the network drops it.
290
+ * Drive keeps the last copy of a part sent twice and completes an upload once; a refusal stands.
291
+ */
292
+ async function sendRetrying(context, target, body, attempt) {
293
+ try {
294
+ return await sendBytes(context, target, body);
295
+ }
296
+ catch (error) {
297
+ // fetch rejects with a TypeError when the request never got a response.
298
+ if (!(error instanceof TypeError) || attempt === PART_ATTEMPTS)
299
+ throw error;
300
+ await context.runtime.sleep(1000 * attempt);
301
+ return await sendRetrying(context, target, body, attempt + 1);
302
+ }
303
+ }
304
+ /** Runs `run` over the items, a few at once; the first error it throws stops the rest. */
305
+ async function eachAtOnce(items, run) {
306
+ let next = 0;
307
+ const worker = async () => {
308
+ for (let item = items[next++]; item !== undefined; item = items[next++]) {
309
+ try {
310
+ await run(item);
311
+ }
312
+ catch (error) {
313
+ next = items.length;
314
+ throw error;
315
+ }
316
+ }
317
+ };
318
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, worker));
319
+ }
320
+ 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":"AAOA,OAAO,EAA4B,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,EAWH,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"}