wrangler 0.0.0-e9ab55a → 0.0.0-e9e9872

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 (201) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +33 -14
  3. package/bin/wrangler.js +126 -25
  4. package/kv-asset-handler.js +1 -0
  5. package/miniflare-dist/index.mjs +4894 -0
  6. package/package.json +159 -125
  7. package/src/__tests__/config-cache-without-cache-dir.test.ts +38 -0
  8. package/src/__tests__/config-cache.test.ts +42 -0
  9. package/src/__tests__/configuration.test.ts +3946 -0
  10. package/src/__tests__/dev.test.tsx +1155 -63
  11. package/src/__tests__/guess-worker-format.test.ts +85 -0
  12. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  13. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  14. package/src/__tests__/helpers/mock-account-id.ts +48 -0
  15. package/src/__tests__/helpers/mock-bin.ts +36 -0
  16. package/src/__tests__/helpers/mock-cfetch.ts +211 -0
  17. package/src/__tests__/helpers/mock-console.ts +97 -0
  18. package/src/__tests__/helpers/mock-dialogs.ts +153 -0
  19. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  20. package/src/__tests__/helpers/mock-istty.ts +74 -0
  21. package/src/__tests__/helpers/mock-kv.ts +33 -0
  22. package/src/__tests__/helpers/mock-oauth-flow.ts +250 -0
  23. package/src/__tests__/helpers/mock-process.ts +39 -0
  24. package/src/__tests__/helpers/mock-stdin.ts +108 -0
  25. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  26. package/src/__tests__/helpers/run-in-tmp.ts +38 -0
  27. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  28. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  29. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  30. package/src/__tests__/https-options.test.ts +163 -0
  31. package/src/__tests__/index.test.ts +259 -284
  32. package/src/__tests__/init.test.ts +1992 -0
  33. package/src/__tests__/jest.setup.ts +108 -10
  34. package/src/__tests__/kv.test.ts +1433 -909
  35. package/src/__tests__/logger.test.ts +141 -0
  36. package/src/__tests__/package-manager.test.ts +353 -0
  37. package/src/__tests__/pages.test.ts +1254 -0
  38. package/src/__tests__/parse.test.ts +330 -0
  39. package/src/__tests__/publish.test.ts +6586 -93
  40. package/src/__tests__/pubsub.test.ts +367 -0
  41. package/src/__tests__/r2.test.ts +233 -0
  42. package/src/__tests__/route.test.ts +45 -0
  43. package/src/__tests__/secret.test.ts +492 -0
  44. package/src/__tests__/tail.test.ts +698 -0
  45. package/src/__tests__/user.test.ts +143 -0
  46. package/src/__tests__/whoami.test.tsx +230 -0
  47. package/src/__tests__/worker-namespace.test.ts +327 -0
  48. package/src/abort.d.ts +3 -0
  49. package/src/api/dev.ts +52 -0
  50. package/src/api/index.ts +1 -0
  51. package/src/bundle-reporter.tsx +29 -0
  52. package/src/bundle.ts +212 -0
  53. package/src/cfetch/index.ts +88 -69
  54. package/src/cfetch/internal.ts +124 -44
  55. package/src/cli.ts +24 -6
  56. package/src/config/README.md +107 -0
  57. package/src/config/config.ts +243 -0
  58. package/src/config/diagnostics.ts +80 -0
  59. package/src/config/environment.ts +453 -0
  60. package/src/config/index.ts +238 -0
  61. package/src/config/validation-helpers.ts +584 -0
  62. package/src/config/validation.ts +1745 -0
  63. package/src/config-cache.ts +83 -0
  64. package/src/create-worker-preview.ts +255 -0
  65. package/src/create-worker-upload-form.ts +263 -0
  66. package/src/dev/dev-vars.ts +36 -0
  67. package/src/dev/dev.tsx +417 -0
  68. package/src/dev/local.tsx +354 -0
  69. package/src/dev/remote.tsx +382 -0
  70. package/src/dev/use-esbuild.ts +150 -0
  71. package/src/dev.tsx +527 -773
  72. package/src/dialogs.tsx +117 -59
  73. package/src/durable.ts +102 -0
  74. package/src/entry.ts +328 -0
  75. package/src/environment-variables.ts +35 -0
  76. package/src/errors.ts +11 -0
  77. package/src/faye-websocket.d.ts +6 -0
  78. package/src/generate.ts +33 -0
  79. package/src/git-client.ts +42 -0
  80. package/src/global-wrangler-config-path.ts +26 -0
  81. package/src/https-options.ts +127 -0
  82. package/src/index.tsx +1869 -1901
  83. package/src/init.ts +549 -0
  84. package/src/inspect.ts +615 -496
  85. package/src/intl-polyfill.d.ts +139 -0
  86. package/src/is-interactive.ts +12 -0
  87. package/src/kv.ts +430 -0
  88. package/src/logger.ts +73 -0
  89. package/src/miniflare-cli/README.md +30 -0
  90. package/src/miniflare-cli/enum-keys.ts +17 -0
  91. package/src/miniflare-cli/index.ts +53 -0
  92. package/src/miniflare-cli/request-context.ts +40 -0
  93. package/src/module-collection.ts +247 -53
  94. package/src/open-in-browser.ts +17 -0
  95. package/src/package-manager.ts +219 -0
  96. package/src/pages/build.tsx +202 -0
  97. package/src/pages/constants.ts +7 -0
  98. package/src/pages/deployments.tsx +101 -0
  99. package/src/pages/dev.tsx +964 -0
  100. package/src/pages/functions/buildPlugin.ts +105 -0
  101. package/src/pages/functions/buildWorker.ts +151 -0
  102. package/src/pages/functions/filepath-routing.test.ts +234 -0
  103. package/src/pages/functions/filepath-routing.ts +189 -0
  104. package/src/pages/functions/identifiers.ts +78 -0
  105. package/src/pages/functions/routes.ts +151 -0
  106. package/src/pages/index.tsx +84 -0
  107. package/src/pages/projects.tsx +157 -0
  108. package/src/pages/publish.tsx +335 -0
  109. package/src/pages/types.ts +40 -0
  110. package/src/pages/upload.tsx +396 -0
  111. package/src/pages/utils.ts +12 -0
  112. package/src/parse.ts +286 -0
  113. package/src/paths.ts +26 -0
  114. package/src/preview.ts +31 -0
  115. package/src/proxy.ts +428 -254
  116. package/src/publish.ts +764 -366
  117. package/src/pubsub/index.ts +286 -0
  118. package/src/pubsub/pubsub-commands.tsx +577 -0
  119. package/src/r2.ts +50 -0
  120. package/src/selfsigned.d.ts +29 -0
  121. package/src/sites.tsx +357 -103
  122. package/src/tail/filters.ts +279 -0
  123. package/src/tail/index.ts +299 -0
  124. package/src/tail/printing.ts +67 -0
  125. package/src/update-check.ts +19 -0
  126. package/src/user/choose-account.tsx +60 -0
  127. package/src/user/env-vars.ts +46 -0
  128. package/src/user/generate-auth-url.ts +33 -0
  129. package/src/user/generate-random-state.ts +16 -0
  130. package/src/user/index.ts +3 -0
  131. package/src/user/user.tsx +1170 -0
  132. package/src/whoami.tsx +90 -0
  133. package/src/worker-namespace.ts +190 -0
  134. package/src/worker.ts +191 -0
  135. package/src/zones.ts +76 -0
  136. package/templates/checked-fetch.js +17 -0
  137. package/templates/gitignore +171 -0
  138. package/templates/new-worker-scheduled.js +17 -0
  139. package/templates/new-worker-scheduled.ts +32 -0
  140. package/templates/new-worker.js +15 -0
  141. package/templates/new-worker.ts +30 -0
  142. package/templates/no-op-worker.js +10 -0
  143. package/templates/pages-template-plugin.ts +155 -0
  144. package/templates/pages-template-worker.ts +161 -0
  145. package/templates/static-asset-facade.js +49 -0
  146. package/templates/tsconfig.json +105 -0
  147. package/wrangler-dist/cli.d.ts +298 -0
  148. package/wrangler-dist/cli.js +107673 -103262
  149. package/pages/functions/buildWorker.ts +0 -62
  150. package/pages/functions/filepath-routing.test.ts +0 -39
  151. package/pages/functions/filepath-routing.ts +0 -228
  152. package/pages/functions/identifiers.ts +0 -78
  153. package/pages/functions/routes.ts +0 -158
  154. package/pages/functions/template-worker.ts +0 -136
  155. package/src/__tests__/mock-cfetch.ts +0 -171
  156. package/src/__tests__/mock-dialogs.ts +0 -65
  157. package/src/__tests__/run-in-tmp.ts +0 -19
  158. package/src/__tests__/run-wrangler.ts +0 -32
  159. package/src/api/form_data.ts +0 -131
  160. package/src/api/preview.ts +0 -128
  161. package/src/api/worker.ts +0 -155
  162. package/src/config.ts +0 -646
  163. package/src/kv.tsx +0 -276
  164. package/src/pages.tsx +0 -1040
  165. package/src/tail.tsx +0 -73
  166. package/src/user.tsx +0 -1027
  167. package/static-asset-facade.js +0 -47
  168. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  169. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  170. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  171. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  172. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  173. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  174. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  175. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  176. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  177. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  178. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  179. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  180. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  181. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  182. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  183. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  184. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  185. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  186. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  187. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  188. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  189. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  190. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  191. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  192. package/vendor/wrangler-mime/LICENSE +0 -21
  193. package/vendor/wrangler-mime/Mime.js +0 -97
  194. package/vendor/wrangler-mime/README.md +0 -187
  195. package/vendor/wrangler-mime/cli.js +0 -46
  196. package/vendor/wrangler-mime/index.js +0 -4
  197. package/vendor/wrangler-mime/lite.js +0 -4
  198. package/vendor/wrangler-mime/package.json +0 -52
  199. package/vendor/wrangler-mime/types/other.js +0 -1
  200. package/vendor/wrangler-mime/types/standard.js +0 -1
  201. package/wrangler-dist/cli.js.map +0 -7
package/src/index.tsx CHANGED
@@ -1,1928 +1,1896 @@
1
- import React from "react";
2
- import { render } from "ink";
3
- import Dev from "./dev";
4
- import { readFile } from "node:fs/promises";
5
- import makeCLI from "yargs";
6
- import type Yargs from "yargs";
7
- import { findUp } from "find-up";
1
+ import path from "node:path";
2
+ import { StringDecoder } from "node:string_decoder";
3
+ import { setTimeout } from "node:timers/promises";
8
4
  import TOML from "@iarna/toml";
9
- import { normaliseAndValidateEnvironmentsConfig } from "./config";
10
- import type { Config } from "./config";
11
- import { confirm, prompt } from "./dialogs";
5
+ import chalk from "chalk";
6
+ import onExit from "signal-exit";
7
+ import supportsColor from "supports-color";
8
+ import { setGlobalDispatcher, ProxyAgent } from "undici";
9
+ import makeCLI from "yargs";
12
10
  import { version as wranglerVersion } from "../package.json";
11
+ import { fetchResult } from "./cfetch";
12
+ import { findWranglerToml, readConfig } from "./config";
13
+ import { createWorkerUploadForm } from "./create-worker-upload-form";
14
+ import { devHandler, devOptions } from "./dev";
15
+ import { confirm, prompt } from "./dialogs";
16
+ import { getEntry } from "./entry";
17
+ import { DeprecationError } from "./errors";
18
+ import { generateHandler, generateOptions } from "./generate";
19
+ import { initOptions, initHandler } from "./init";
13
20
  import {
14
- login,
15
- logout,
16
- listScopes,
17
- initialise as initialiseUserConfig,
18
- loginOrRefreshIfRequired,
19
- getAccountId,
20
- } from "./user";
21
- import {
22
- getNamespaceId,
23
- listNamespaces,
24
- listNamespaceKeys,
25
- putKeyValue,
26
- putBulkKeyValue,
27
- deleteBulkKeyValue,
28
- createNamespace,
29
- isValidNamespaceBinding,
21
+ getKVNamespaceId,
22
+ listKVNamespaces,
23
+ listKVNamespaceKeys,
24
+ putKVKeyValue,
25
+ putKVBulkKeyValue,
26
+ deleteKVBulkKeyValue,
27
+ createKVNamespace,
28
+ isValidKVNamespaceBinding,
29
+ getKVKeyValue,
30
+ isKVKeyValue,
31
+ unexpectedKVKeyValueProps,
32
+ deleteKVNamespace,
33
+ deleteKVKeyValue,
30
34
  } from "./kv";
31
-
35
+ import { logger } from "./logger";
32
36
  import { pages } from "./pages";
33
-
34
- import { fetchResult, fetchRaw } from "./cfetch";
35
-
37
+ import {
38
+ formatMessage,
39
+ ParseError,
40
+ parseJSON,
41
+ readFileSync,
42
+ readFileSyncToBuffer,
43
+ } from "./parse";
44
+ import { previewHandler, previewOptions } from "./preview";
36
45
  import publish from "./publish";
37
- import path from "path/posix";
38
- import { writeFile } from "node:fs/promises";
39
- import { toFormData } from "./api/form_data";
46
+ import { pubSubCommands } from "./pubsub/pubsub-commands";
47
+ import { createR2Bucket, deleteR2Bucket, listR2Buckets } from "./r2";
48
+ import { getAssetPaths, getSiteAssetPaths } from "./sites";
49
+ import {
50
+ createTail,
51
+ jsonPrintLogs,
52
+ prettyPrintLogs,
53
+ translateCLICommandToFilterMessage,
54
+ } from "./tail";
55
+ import { updateCheck } from "./update-check";
56
+ import {
57
+ login,
58
+ logout,
59
+ listScopes,
60
+ validateScopeKeys,
61
+ requireAuth,
62
+ } from "./user";
63
+ import { whoami } from "./whoami";
40
64
 
41
- import { createTail } from "./tail";
42
- import onExit from "signal-exit";
43
- import { setTimeout } from "node:timers/promises";
44
- import * as fs from "node:fs";
45
- import { execa } from "execa";
65
+ import { workerNamespaceCommands } from "./worker-namespace";
66
+ import type { Config } from "./config";
67
+ import type { KeyValue } from "./kv";
68
+ import type { TailCLIFilters } from "./tail";
69
+ import type { RawData } from "ws";
70
+ import type { CommandModule } from "yargs";
71
+ import type Yargs from "yargs";
72
+
73
+ export type ConfigPath = string | undefined;
46
74
 
47
75
  const resetColor = "\x1b[0m";
48
76
  const fgGreenColor = "\x1b[32m";
77
+ export const DEFAULT_LOCAL_PORT = 8787;
49
78
 
50
- async function readConfig(configPath?: string): Promise<Config> {
51
- const config: Config = {};
52
- if (!configPath) {
53
- configPath = await findUp("wrangler.toml");
54
- // TODO - terminate this early instead of going all the way to the root
55
- }
79
+ const proxy =
80
+ process.env.https_proxy ||
81
+ process.env.HTTPS_PROXY ||
82
+ process.env.http_proxy ||
83
+ process.env.HTTP_PROXY ||
84
+ undefined;
85
+
86
+ if (proxy) {
87
+ setGlobalDispatcher(new ProxyAgent(proxy));
88
+ }
56
89
 
57
- if (configPath) {
58
- const tml: string = await readFile(configPath, "utf-8");
59
- const parsed = TOML.parse(tml) as Config;
60
- Object.assign(config, parsed);
61
- }
90
+ export function getRules(config: Config): Config["rules"] {
91
+ const rules = config.rules ?? config.build?.upload?.rules ?? [];
92
+
93
+ if (config.rules && config.build?.upload?.rules) {
94
+ throw new Error(
95
+ `You cannot configure both [rules] and [build.upload.rules] in your wrangler.toml. Delete the \`build.upload\` section.`
96
+ );
97
+ }
98
+
99
+ if (config.build?.upload?.rules) {
100
+ logger.warn(
101
+ `Deprecation: The \`build.upload.rules\` config field is no longer used, the rules should be specified via the \`rules\` config field. Delete the \`build.upload\` field from the configuration file, and add this:
102
+
103
+ ${TOML.stringify({ rules: config.build.upload.rules })}`
104
+ );
105
+ }
106
+ return rules;
107
+ }
62
108
 
63
- normaliseAndValidateEnvironmentsConfig(config);
109
+ export async function printWranglerBanner() {
110
+ // Let's not print this in tests
111
+ if (typeof jest !== "undefined") {
112
+ return;
113
+ }
114
+
115
+ const text = ` ⛅️ wrangler ${wranglerVersion} ${await updateCheck()}`;
116
+
117
+ logger.log(
118
+ text +
119
+ "\n" +
120
+ (supportsColor.stdout
121
+ ? chalk.hex("#FF8800")("-".repeat(text.length))
122
+ : "-".repeat(text.length))
123
+ );
124
+ }
64
125
 
65
- if ("experimental_services" in config) {
66
- console.warn(
67
- "The experimental_services field is only for cloudflare internal usage right now, and is subject to change. Please do not use this on production projects"
68
- );
69
- }
126
+ export function isLegacyEnv(config: Config): boolean {
127
+ // We only read from config here, because we've already accounted for
128
+ // args["legacy-env"] in https://github.com/cloudflare/wrangler2/blob/b24aeb5722370c2e04bce97a84a1fa1e55725d79/packages/wrangler/src/config/validation.ts#L94-L98
129
+ return config.legacy_env;
130
+ }
70
131
 
71
- // todo: validate, add defaults
72
- // let's just do some basics for now
132
+ export function getScriptName(
133
+ args: { name: string | undefined; env: string | undefined },
134
+ config: Config
135
+ ): string | undefined {
136
+ if (args.name && isLegacyEnv(config) && args.env) {
137
+ throw new CommandLineArgsError(
138
+ "In legacy environment mode you cannot use --name and --env together. If you want to specify a Worker name for a specific environment you can add the following to your wrangler.toml config:" +
139
+ `
140
+ [env.${args.env}]
141
+ name = "${args.name}"
142
+ `
143
+ );
144
+ }
145
+
146
+ return args.name ?? config.name;
147
+ }
73
148
 
74
- // @ts-expect-error we're being sneaky here for now
75
- config.__path__ = configPath;
149
+ /**
150
+ * Alternative to the getScriptName() because special Legacy cases allowed "name", and "env" together in Wrangler1
151
+ */
152
+ function getLegacyScriptName(
153
+ args: { name: string | undefined; env: string | undefined },
154
+ config: Config
155
+ ) {
156
+ return args.name && args.env && isLegacyEnv(config)
157
+ ? `${args.name}-${args.env}`
158
+ : args.name ?? config.name;
159
+ }
76
160
 
77
- return config;
161
+ /**
162
+ * Get a promise to the streamed input from stdin.
163
+ *
164
+ * This function can be used to grab the incoming stream of data from, say,
165
+ * piping the output of another process into the wrangler process.
166
+ */
167
+ function readFromStdin(): Promise<string> {
168
+ return new Promise((resolve, reject) => {
169
+ const stdin = process.stdin;
170
+ const chunks: string[] = [];
171
+
172
+ // When there is data ready to be read, the `readable` event will be triggered.
173
+ // In the handler for `readable` we call `read()` over and over until all the available data has been read.
174
+ stdin.on("readable", () => {
175
+ let chunk;
176
+ while (null !== (chunk = stdin.read())) {
177
+ chunks.push(chunk);
178
+ }
179
+ });
180
+
181
+ // When the streamed data is complete the `end` event will be triggered.
182
+ // In the handler for `end` we join the chunks together and resolve the promise.
183
+ stdin.on("end", () => {
184
+ resolve(chunks.join(""));
185
+ });
186
+
187
+ // If there is an `error` event then the handler will reject the promise.
188
+ stdin.on("error", (err) => {
189
+ reject(err);
190
+ });
191
+ });
78
192
  }
79
193
 
80
194
  // a helper to demand one of a set of options
81
195
  // via https://github.com/yargs/yargs/issues/1093#issuecomment-491299261
82
196
  function demandOneOfOption(...options: string[]) {
83
- return function (argv: Yargs.Arguments) {
84
- const count = options.filter((option) => argv[option]).length;
85
- const lastOption = options.pop();
86
-
87
- if (count === 0) {
88
- throw new CommandLineArgsError(
89
- `Exactly one of the arguments ${options.join(
90
- ", "
91
- )} and ${lastOption} is required`
92
- );
93
- } else if (count > 1) {
94
- throw new CommandLineArgsError(
95
- `Arguments ${options.join(
96
- ", "
97
- )} and ${lastOption} are mutually exclusive`
98
- );
99
- }
100
-
101
- return true;
102
- };
197
+ return function (argv: Yargs.Arguments) {
198
+ const count = options.filter((option) => argv[option]).length;
199
+ const lastOption = options.pop();
200
+
201
+ if (count === 0) {
202
+ throw new CommandLineArgsError(
203
+ `Exactly one of the arguments ${options.join(
204
+ ", "
205
+ )} and ${lastOption} is required`
206
+ );
207
+ } else if (count > 1) {
208
+ throw new CommandLineArgsError(
209
+ `Arguments ${options.join(
210
+ ", "
211
+ )} and ${lastOption} are mutually exclusive`
212
+ );
213
+ }
214
+
215
+ return true;
216
+ };
103
217
  }
104
218
 
105
- class CommandLineArgsError extends Error {}
106
- class DeprecationError extends Error {}
107
- class NotImplementedError extends Error {}
219
+ /**
220
+ * Remove trailing white space from inputs.
221
+ * Matching Wrangler legacy behavior with handling inputs
222
+ */
223
+ function trimTrailingWhitespace(str: string) {
224
+ return str.trimEnd();
225
+ }
226
+
227
+ export class CommandLineArgsError extends Error {}
228
+
229
+ function createCLIParser(argv: string[]) {
230
+ const wrangler = makeCLI(argv)
231
+ .strict()
232
+ // We handle errors ourselves in a try-catch around `yargs.parse`.
233
+ // If you want the "help info" to be displayed then throw an instance of `CommandLineArgsError`.
234
+ // Otherwise we just log the error that was thrown without any "help info".
235
+ .showHelpOnFail(false)
236
+ .fail((msg, error) => {
237
+ if (!error || error.name === "YError") {
238
+ // If there is no error or the error is a "YError", then this came from yargs own validation
239
+ // Wrap it in a `CommandLineArgsError` so that we can handle it appropriately further up.
240
+ error = new CommandLineArgsError(msg);
241
+ }
242
+ throw error;
243
+ })
244
+ .scriptName("wrangler")
245
+ .wrap(null);
246
+
247
+ // Default help command that supports the subcommands
248
+ const subHelp: CommandModule = {
249
+ command: ["*"],
250
+ handler: async (args) => {
251
+ setImmediate(() =>
252
+ wrangler.parse([...args._.map((a) => `${a}`), "--help"])
253
+ );
254
+ },
255
+ };
256
+ wrangler.command(
257
+ ["*"],
258
+ false,
259
+ () => {},
260
+ (args) => {
261
+ if (args._.length > 0) {
262
+ throw new CommandLineArgsError(`Unknown command: ${args._}.`);
263
+ } else {
264
+ wrangler.showHelp("log");
265
+ }
266
+ }
267
+ );
268
+
269
+ // You will note that we use the form for all commands where we use the builder function
270
+ // to define options and subcommands.
271
+ // Further we return the result of this builder even though it's not completely necessary.
272
+ // The reason is that it's required for type inference of the args in the handle function.
273
+ // I wish we could enforce this pattern, but this comment will have to do for now.
274
+ // (It's also annoying that choices[] doesn't get inferred as an enum. 🤷‍♂.)
275
+
276
+ // [DEPRECATED] generate
277
+ wrangler.command(
278
+ // we definitely want to move away from us cloning github templates
279
+ // we can do something better here, let's see
280
+ "generate [name] [template]",
281
+ false,
282
+ generateOptions,
283
+ generateHandler
284
+ );
285
+
286
+ // init
287
+ wrangler.command(
288
+ "init [name]",
289
+ "📥 Create a wrangler.toml configuration file",
290
+ initOptions,
291
+ initHandler
292
+ );
293
+
294
+ // build
295
+ wrangler.command(
296
+ "build",
297
+ false,
298
+ (yargs) => {
299
+ return yargs.option("env", {
300
+ describe: "Perform on a specific environment",
301
+ type: "string",
302
+ });
303
+ },
304
+ async (buildArgs) => {
305
+ // "[DEPRECATED] 🦀 Build your project (if applicable)",
306
+
307
+ const envFlag = buildArgs.env ? ` --env=${buildArgs.env}` : "";
308
+ logger.log(
309
+ formatMessage({
310
+ kind: "warning",
311
+ text: "Deprecation: `wrangler build` has been deprecated.",
312
+ notes: [
313
+ {
314
+ text: "Please refer to https://developers.cloudflare.com/workers/wrangler/migration/deprecations/#build for more information.",
315
+ },
316
+ {
317
+ text: `Attempting to run \`wrangler publish --dry-run --outdir=dist${envFlag}\` for you instead:`,
318
+ },
319
+ ],
320
+ })
321
+ );
322
+
323
+ await createCLIParser([
324
+ "publish",
325
+ "--dry-run",
326
+ "--outdir=dist",
327
+ ...(buildArgs.env ? ["--env", buildArgs.env] : []),
328
+ ]).parse();
329
+ }
330
+ );
331
+
332
+ // config
333
+ wrangler.command(
334
+ "config",
335
+ false,
336
+ () => {},
337
+ () => {
338
+ // "🕵️ Authenticate Wrangler with a Cloudflare API Token",
339
+ throw new DeprecationError(
340
+ "`wrangler config` has been deprecated, please refer to https://developers.cloudflare.com/workers/wrangler/migration/deprecations/#config for alternatives"
341
+ );
342
+ }
343
+ );
344
+
345
+ // dev
346
+ wrangler.command(
347
+ "dev [script]",
348
+ "👂 Start a local server for developing your worker",
349
+ devOptions,
350
+ devHandler
351
+ );
352
+
353
+ // publish
354
+ wrangler.command(
355
+ "publish [script]",
356
+ "🆙 Publish your Worker to Cloudflare.",
357
+ (yargs) => {
358
+ return (
359
+ yargs
360
+ .option("env", {
361
+ type: "string",
362
+ requiresArg: true,
363
+ describe: "Perform on a specific environment",
364
+ alias: "e",
365
+ })
366
+ .positional("script", {
367
+ describe: "The path to an entry point for your worker",
368
+ type: "string",
369
+ requiresArg: true,
370
+ })
371
+ .option("name", {
372
+ describe: "Name of the worker",
373
+ type: "string",
374
+ requiresArg: true,
375
+ })
376
+ // We want to have a --no-bundle flag, but yargs requires that
377
+ // we also have a --bundle flag (that it adds the --no to by itself)
378
+ // So we make a --bundle flag, but hide it, and then add a --no-bundle flag
379
+ // that's visible to the user but doesn't "do" anything.
380
+ .option("bundle", {
381
+ describe: "Run wrangler's compilation step before publishing",
382
+ type: "boolean",
383
+ hidden: true,
384
+ })
385
+ .option("no-bundle", {
386
+ describe: "Skip internal build steps and directly publish Worker",
387
+ type: "boolean",
388
+ default: false,
389
+ })
390
+ .option("outdir", {
391
+ describe: "Output directory for the bundled worker",
392
+ type: "string",
393
+ requiresArg: true,
394
+ })
395
+ .option("format", {
396
+ choices: ["modules", "service-worker"] as const,
397
+ describe: "Choose an entry type",
398
+ deprecated: true,
399
+ })
400
+ .option("compatibility-date", {
401
+ describe: "Date to use for compatibility checks",
402
+ type: "string",
403
+ requiresArg: true,
404
+ })
405
+ .option("compatibility-flags", {
406
+ describe: "Flags to use for compatibility checks",
407
+ alias: "compatibility-flag",
408
+ type: "string",
409
+ requiresArg: true,
410
+ array: true,
411
+ })
412
+ .option("latest", {
413
+ describe: "Use the latest version of the worker runtime",
414
+ type: "boolean",
415
+ default: false,
416
+ })
417
+ .option("experimental-public", {
418
+ describe: "Static assets to be served",
419
+ type: "string",
420
+ requiresArg: true,
421
+ deprecated: true,
422
+ hidden: true,
423
+ })
424
+ .option("assets", {
425
+ describe: "Static assets to be served",
426
+ type: "string",
427
+ requiresArg: true,
428
+ })
429
+ .option("site", {
430
+ describe: "Root folder of static assets for Workers Sites",
431
+ type: "string",
432
+ requiresArg: true,
433
+ })
434
+ .option("site-include", {
435
+ describe:
436
+ "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.",
437
+ type: "string",
438
+ requiresArg: true,
439
+ array: true,
440
+ })
441
+ .option("site-exclude", {
442
+ describe:
443
+ "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.",
444
+ type: "string",
445
+ requiresArg: true,
446
+ array: true,
447
+ })
448
+ .option("triggers", {
449
+ describe: "cron schedules to attach",
450
+ alias: ["schedule", "schedules"],
451
+ type: "string",
452
+ requiresArg: true,
453
+ array: true,
454
+ })
455
+ .option("routes", {
456
+ describe: "Routes to upload",
457
+ alias: "route",
458
+ type: "string",
459
+ requiresArg: true,
460
+ array: true,
461
+ })
462
+ .option("jsx-factory", {
463
+ describe: "The function that is called for each JSX element",
464
+ type: "string",
465
+ requiresArg: true,
466
+ })
467
+ .option("jsx-fragment", {
468
+ describe: "The function that is called for each JSX fragment",
469
+ type: "string",
470
+ requiresArg: true,
471
+ })
472
+ .option("tsconfig", {
473
+ describe: "Path to a custom tsconfig.json file",
474
+ type: "string",
475
+ requiresArg: true,
476
+ })
477
+ .option("minify", {
478
+ describe: "Minify the Worker",
479
+ type: "boolean",
480
+ })
481
+ .option("node-compat", {
482
+ describe: "Enable node.js compatibility",
483
+ type: "boolean",
484
+ })
485
+ .option("dry-run", {
486
+ describe: "Don't actually publish",
487
+ type: "boolean",
488
+ })
489
+ .option("legacy-env", {
490
+ type: "boolean",
491
+ describe: "Use legacy environments",
492
+ hidden: true,
493
+ })
494
+ );
495
+ },
496
+ async (args) => {
497
+ await printWranglerBanner();
498
+
499
+ const configPath =
500
+ (args.config as ConfigPath) ||
501
+ (args.script && findWranglerToml(path.dirname(args.script)));
502
+ const config = readConfig(configPath, args);
503
+ const entry = await getEntry(args, config, "publish");
504
+
505
+ if (args.public) {
506
+ throw new Error("The --public field has been renamed to --assets");
507
+ }
508
+ if (args["experimental-public"]) {
509
+ throw new Error(
510
+ "The --experimental-public field has been renamed to --assets"
511
+ );
512
+ }
513
+
514
+ if ((args.assets || config.assets) && (args.site || config.site)) {
515
+ throw new Error(
516
+ "Cannot use Assets and Workers Sites in the same Worker."
517
+ );
518
+ }
519
+
520
+ if (args.assets) {
521
+ logger.warn(
522
+ "The --assets argument is experimental and may change or break at any time"
523
+ );
524
+ }
525
+
526
+ if (args.latest) {
527
+ logger.warn(
528
+ "Using the latest version of the Workers runtime. To silence this warning, please choose a specific version of the runtime with --compatibility-date, or add a compatibility_date to your wrangler.toml.\n"
529
+ );
530
+ }
531
+
532
+ const accountId = args.dryRun ? undefined : await requireAuth(config);
533
+
534
+ const assetPaths =
535
+ args.assets || config.assets
536
+ ? getAssetPaths(config, args.assets)
537
+ : getSiteAssetPaths(
538
+ config,
539
+ args.site,
540
+ args.siteInclude,
541
+ args.siteExclude
542
+ );
543
+
544
+ await publish({
545
+ config,
546
+ accountId,
547
+ name: getScriptName(args, config),
548
+ rules: getRules(config),
549
+ entry,
550
+ env: args.env,
551
+ compatibilityDate: args.latest
552
+ ? new Date().toISOString().substring(0, 10)
553
+ : args["compatibility-date"],
554
+ compatibilityFlags: args["compatibility-flags"],
555
+ triggers: args.triggers,
556
+ jsxFactory: args["jsx-factory"],
557
+ jsxFragment: args["jsx-fragment"],
558
+ tsconfig: args.tsconfig,
559
+ routes: args.routes,
560
+ assetPaths,
561
+ legacyEnv: isLegacyEnv(config),
562
+ minify: args.minify,
563
+ nodeCompat: args.nodeCompat,
564
+ isWorkersSite: Boolean(args.site || config.site),
565
+ outDir: args.outdir,
566
+ dryRun: args.dryRun,
567
+ noBundle: !(args.bundle ?? !config.no_bundle),
568
+ });
569
+ }
570
+ );
571
+
572
+ // tail
573
+ wrangler.command(
574
+ "tail [name]",
575
+ "🦚 Starts a log tailing session for a published Worker.",
576
+ (yargs) => {
577
+ return yargs
578
+ .positional("name", {
579
+ describe: "Name of the worker",
580
+ type: "string",
581
+ })
582
+ .option("format", {
583
+ default: process.stdout.isTTY ? "pretty" : "json",
584
+ choices: ["json", "pretty"],
585
+ describe: "The format of log entries",
586
+ })
587
+ .option("status", {
588
+ choices: ["ok", "error", "canceled"],
589
+ describe: "Filter by invocation status",
590
+ array: true,
591
+ })
592
+ .option("header", {
593
+ type: "string",
594
+ requiresArg: true,
595
+ describe: "Filter by HTTP header",
596
+ })
597
+ .option("method", {
598
+ type: "string",
599
+ requiresArg: true,
600
+ describe: "Filter by HTTP method",
601
+ array: true,
602
+ })
603
+ .option("sampling-rate", {
604
+ type: "number",
605
+ describe: "Adds a percentage of requests to log sampling rate",
606
+ })
607
+ .option("search", {
608
+ type: "string",
609
+ requiresArg: true,
610
+ describe: "Filter by a text match in console.log messages",
611
+ })
612
+ .option("ip", {
613
+ type: "string",
614
+ requiresArg: true,
615
+ describe:
616
+ 'Filter by the IP address the request originates from. Use "self" to filter for your own IP',
617
+ array: true,
618
+ })
619
+ .option("env", {
620
+ type: "string",
621
+ requiresArg: true,
622
+ describe: "Perform on a specific environment",
623
+ alias: "e",
624
+ })
625
+ .option("debug", {
626
+ type: "boolean",
627
+ hidden: true,
628
+ default: false,
629
+ describe:
630
+ "If a log would have been filtered out, send it through anyway alongside the filter which would have blocked it.",
631
+ })
632
+ .option("legacy-env", {
633
+ type: "boolean",
634
+ describe: "Use legacy environments",
635
+ hidden: true,
636
+ });
637
+ },
638
+ async (args) => {
639
+ if (args.format === "pretty") {
640
+ await printWranglerBanner();
641
+ }
642
+ const config = readConfig(args.config as ConfigPath, args);
643
+
644
+ const scriptName = getLegacyScriptName(args, config);
645
+
646
+ if (!scriptName) {
647
+ throw new Error(
648
+ "Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with `wrangler tail <worker-name>`"
649
+ );
650
+ }
651
+
652
+ const accountId = await requireAuth(config);
653
+
654
+ const cliFilters: TailCLIFilters = {
655
+ status: args.status as ("ok" | "error" | "canceled")[] | undefined,
656
+ header: args.header,
657
+ method: args.method,
658
+ samplingRate: args["sampling-rate"],
659
+ search: args.search,
660
+ clientIp: args.ip,
661
+ };
662
+
663
+ const filters = translateCLICommandToFilterMessage(
664
+ cliFilters,
665
+ args.debug
666
+ );
667
+
668
+ const { tail, expiration, deleteTail } = await createTail(
669
+ accountId,
670
+ scriptName,
671
+ filters,
672
+ !isLegacyEnv(config) ? args.env : undefined
673
+ );
674
+
675
+ const scriptDisplayName = `${scriptName}${
676
+ args.env && !isLegacyEnv(config) ? ` (${args.env})` : ""
677
+ }`;
678
+
679
+ if (args.format === "pretty") {
680
+ logger.log(
681
+ `Successfully created tail, expires at ${expiration.toLocaleString()}`
682
+ );
683
+ }
684
+
685
+ onExit(async () => {
686
+ tail.terminate();
687
+ await deleteTail();
688
+ });
689
+
690
+ const printLog: (data: RawData) => void =
691
+ args.format === "pretty" ? prettyPrintLogs : jsonPrintLogs;
692
+
693
+ tail.on("message", printLog);
694
+
695
+ while (tail.readyState !== tail.OPEN) {
696
+ switch (tail.readyState) {
697
+ case tail.CONNECTING:
698
+ await setTimeout(100);
699
+ break;
700
+ case tail.CLOSING:
701
+ await setTimeout(100);
702
+ break;
703
+ case tail.CLOSED:
704
+ throw new Error(
705
+ `Connection to ${scriptDisplayName} closed unexpectedly.`
706
+ );
707
+ }
708
+ }
709
+
710
+ if (args.format === "pretty") {
711
+ logger.log(`Connected to ${scriptDisplayName}, waiting for logs...`);
712
+ }
713
+
714
+ tail.on("close", async () => {
715
+ tail.terminate();
716
+ await deleteTail();
717
+ });
718
+ }
719
+ );
720
+
721
+ // preview
722
+ wrangler.command(
723
+ "preview [method] [body]",
724
+ false,
725
+ previewOptions,
726
+ previewHandler
727
+ );
728
+
729
+ // [DEPRECATED] route
730
+ wrangler.command(
731
+ "route",
732
+ false, // I think we want to hide this command
733
+ // "➡️ List or delete worker routes",
734
+ (routeYargs) => {
735
+ return routeYargs
736
+ .command(
737
+ "list",
738
+ "List the routes associated with a zone",
739
+ (yargs) => {
740
+ return yargs
741
+ .option("env", {
742
+ type: "string",
743
+ requiresArg: true,
744
+ describe: "Perform on a specific environment",
745
+ })
746
+ .option("zone", {
747
+ type: "string",
748
+ requiresArg: true,
749
+ describe: "Zone id",
750
+ })
751
+ .positional("zone", {
752
+ describe: "Zone id",
753
+ type: "string",
754
+ });
755
+ },
756
+ () => {
757
+ // "👯 [DEPRECATED]. Use wrangler.toml to manage routes.
758
+ const deprecationNotice =
759
+ "`wrangler route list` has been deprecated.";
760
+ const futureRoutes =
761
+ "Refer to wrangler.toml for a list of routes the worker will be deployed to upon publishing.";
762
+ const presentRoutes =
763
+ "Refer to the Cloudflare Dashboard to see the routes this worker is currently running on.";
764
+ throw new DeprecationError(
765
+ `${deprecationNotice}\n${futureRoutes}\n${presentRoutes}`
766
+ );
767
+ }
768
+ )
769
+ .command(
770
+ "delete [id]",
771
+ "Delete a route associated with a zone",
772
+ (yargs) => {
773
+ return yargs
774
+ .positional("id", {
775
+ describe: "The hash of the route ID to delete.",
776
+ type: "string",
777
+ })
778
+ .option("zone", {
779
+ type: "string",
780
+ requiresArg: true,
781
+ describe: "zone id",
782
+ })
783
+ .option("env", {
784
+ type: "string",
785
+ requiresArg: true,
786
+ describe: "Perform on a specific environment",
787
+ });
788
+ },
789
+ () => {
790
+ // "👯 [DEPRECATED]. Use wrangler.toml to manage routes.
791
+ const deprecationNotice =
792
+ "`wrangler route delete` has been deprecated.";
793
+ const shouldDo =
794
+ "Remove the unwanted route(s) from wrangler.toml and run `wrangler publish` to remove your worker from those routes.";
795
+ throw new DeprecationError(`${deprecationNotice}\n${shouldDo}`);
796
+ }
797
+ );
798
+ },
799
+ () => {
800
+ // "👯 [DEPRECATED]. Use wrangler.toml to manage routes.
801
+ const deprecationNotice = "`wrangler route` has been deprecated.";
802
+ const shouldDo =
803
+ "Please use wrangler.toml and/or `wrangler publish --routes` to modify routes";
804
+ throw new DeprecationError(`${deprecationNotice}\n${shouldDo}`);
805
+ }
806
+ );
807
+
808
+ // subdomain
809
+ wrangler.command(
810
+ "subdomain [name]",
811
+ false,
812
+ // "👷 Create or change your workers.dev subdomain.",
813
+ (yargs) => {
814
+ return yargs.positional("name", { type: "string" });
815
+ },
816
+ () => {
817
+ throw new DeprecationError(
818
+ "`wrangler subdomain` has been deprecated, please refer to https://developers.cloudflare.com/workers/wrangler/migration/deprecations/#subdomain for alternatives"
819
+ );
820
+ }
821
+ );
822
+
823
+ // secret
824
+ wrangler.command(
825
+ "secret",
826
+ "🤫 Generate a secret that can be referenced in a Worker",
827
+ (secretYargs) => {
828
+ return secretYargs
829
+ .command(subHelp)
830
+ .option("legacy-env", {
831
+ type: "boolean",
832
+ describe: "Use legacy environments",
833
+ hidden: true,
834
+ })
835
+ .command(
836
+ "put <key>",
837
+ "Create or update a secret variable for a Worker",
838
+ (yargs) => {
839
+ return yargs
840
+ .positional("key", {
841
+ describe: "The variable name to be accessible in the Worker",
842
+ type: "string",
843
+ })
844
+ .option("name", {
845
+ describe: "Name of the Worker",
846
+ type: "string",
847
+ requiresArg: true,
848
+ })
849
+ .option("env", {
850
+ type: "string",
851
+ requiresArg: true,
852
+ describe:
853
+ "Binds the secret to the Worker of the specific environment",
854
+ alias: "e",
855
+ });
856
+ },
857
+ async (args) => {
858
+ await printWranglerBanner();
859
+ const config = readConfig(args.config as ConfigPath, args);
860
+
861
+ const scriptName = getLegacyScriptName(args, config);
862
+ if (!scriptName) {
863
+ throw new Error(
864
+ "Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with `--name <worker-name>`"
865
+ );
866
+ }
867
+
868
+ const accountId = await requireAuth(config);
869
+
870
+ const isInteractive = process.stdin.isTTY;
871
+ const secretValue = trimTrailingWhitespace(
872
+ isInteractive
873
+ ? await prompt("Enter a secret value:", "password")
874
+ : await readFromStdin()
875
+ );
876
+
877
+ logger.log(
878
+ `🌀 Creating the secret for the Worker "${scriptName}" ${
879
+ args.env && !isLegacyEnv(config) ? `(${args.env})` : ""
880
+ }`
881
+ );
882
+
883
+ async function submitSecret() {
884
+ const url =
885
+ !args.env || isLegacyEnv(config)
886
+ ? `/accounts/${accountId}/workers/scripts/${scriptName}/secrets`
887
+ : `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}/secrets`;
888
+
889
+ return await fetchResult(url, {
890
+ method: "PUT",
891
+ headers: { "Content-Type": "application/json" },
892
+ body: JSON.stringify({
893
+ name: args.key,
894
+ text: secretValue,
895
+ type: "secret_text",
896
+ }),
897
+ });
898
+ }
899
+
900
+ const createDraftWorker = async () => {
901
+ // TODO: log a warning
902
+ await fetchResult(
903
+ !isLegacyEnv(config) && args.env
904
+ ? `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}`
905
+ : `/accounts/${accountId}/workers/scripts/${scriptName}`,
906
+ {
907
+ method: "PUT",
908
+ body: createWorkerUploadForm({
909
+ name: scriptName,
910
+ main: {
911
+ name: scriptName,
912
+ content: `export default { fetch() {} }`,
913
+ type: "esm",
914
+ },
915
+ bindings: {
916
+ kv_namespaces: [],
917
+ vars: {},
918
+ durable_objects: { bindings: [] },
919
+ r2_buckets: [],
920
+ services: [],
921
+ wasm_modules: {},
922
+ text_blobs: {},
923
+ data_blobs: {},
924
+ worker_namespaces: [],
925
+ unsafe: [],
926
+ },
927
+ modules: [],
928
+ migrations: undefined,
929
+ compatibility_date: undefined,
930
+ compatibility_flags: undefined,
931
+ usage_model: undefined,
932
+ }),
933
+ }
934
+ );
935
+ };
936
+
937
+ function isMissingWorkerError(e: unknown): e is { code: 10007 } {
938
+ return (
939
+ typeof e === "object" &&
940
+ e !== null &&
941
+ (e as { code: number }).code === 10007
942
+ );
943
+ }
944
+
945
+ try {
946
+ await submitSecret();
947
+ } catch (e) {
948
+ if (isMissingWorkerError(e)) {
949
+ // create a draft worker and try again
950
+ await createDraftWorker();
951
+ await submitSecret();
952
+ // TODO: delete the draft worker if this failed too?
953
+ } else {
954
+ throw e;
955
+ }
956
+ }
957
+
958
+ logger.log(`✨ Success! Uploaded secret ${args.key}`);
959
+ }
960
+ )
961
+ .command(
962
+ "delete <key>",
963
+ "Delete a secret variable from a Worker",
964
+ async (yargs) => {
965
+ await printWranglerBanner();
966
+ return yargs
967
+ .positional("key", {
968
+ describe: "The variable name to be accessible in the Worker",
969
+ type: "string",
970
+ })
971
+ .option("name", {
972
+ describe: "Name of the Worker",
973
+ type: "string",
974
+ requiresArg: true,
975
+ })
976
+ .option("env", {
977
+ type: "string",
978
+ requiresArg: true,
979
+ describe:
980
+ "Binds the secret to the Worker of the specific environment",
981
+ alias: "e",
982
+ });
983
+ },
984
+ async (args) => {
985
+ const config = readConfig(args.config as ConfigPath, args);
986
+
987
+ const scriptName = getLegacyScriptName(args, config);
988
+ if (!scriptName) {
989
+ throw new Error(
990
+ "Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with `--name <worker-name>`"
991
+ );
992
+ }
993
+
994
+ const accountId = await requireAuth(config);
995
+
996
+ if (
997
+ await confirm(
998
+ `Are you sure you want to permanently delete the secret ${
999
+ args.key
1000
+ } on the Worker ${scriptName}${
1001
+ args.env && !isLegacyEnv(config) ? ` (${args.env})` : ""
1002
+ }?`
1003
+ )
1004
+ ) {
1005
+ logger.log(
1006
+ `🌀 Deleting the secret ${
1007
+ args.key
1008
+ } on the Worker ${scriptName}${
1009
+ args.env && !isLegacyEnv(config) ? ` (${args.env})` : ""
1010
+ }`
1011
+ );
1012
+
1013
+ const url =
1014
+ !args.env || isLegacyEnv(config)
1015
+ ? `/accounts/${accountId}/workers/scripts/${scriptName}/secrets`
1016
+ : `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1017
+
1018
+ await fetchResult(`${url}/${args.key}`, { method: "DELETE" });
1019
+ logger.log(`✨ Success! Deleted secret ${args.key}`);
1020
+ }
1021
+ }
1022
+ )
1023
+ .command(
1024
+ "list",
1025
+ "List all secrets for a Worker",
1026
+ (yargs) => {
1027
+ return yargs
1028
+ .option("name", {
1029
+ describe: "Name of the Worker",
1030
+ type: "string",
1031
+ requiresArg: true,
1032
+ })
1033
+ .option("env", {
1034
+ type: "string",
1035
+ requiresArg: true,
1036
+ describe:
1037
+ "Binds the secret to the Worker of the specific environment.",
1038
+ alias: "e",
1039
+ });
1040
+ },
1041
+ async (args) => {
1042
+ const config = readConfig(args.config as ConfigPath, args);
1043
+
1044
+ const scriptName = getLegacyScriptName(args, config);
1045
+ if (!scriptName) {
1046
+ throw new Error(
1047
+ "Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with `--name <worker-name>`"
1048
+ );
1049
+ }
1050
+
1051
+ const accountId = await requireAuth(config);
1052
+
1053
+ const url =
1054
+ !args.env || isLegacyEnv(config)
1055
+ ? `/accounts/${accountId}/workers/scripts/${scriptName}/secrets`
1056
+ : `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1057
+
1058
+ logger.log(JSON.stringify(await fetchResult(url), null, " "));
1059
+ }
1060
+ );
1061
+ }
1062
+ );
1063
+
1064
+ // kv
1065
+ // :namespace
1066
+ wrangler.command(
1067
+ "kv:namespace",
1068
+ "🗂️ Interact with your Workers KV Namespaces",
1069
+ (namespaceYargs) => {
1070
+ return namespaceYargs
1071
+ .command(subHelp)
1072
+ .command(
1073
+ "create <namespace>",
1074
+ "Create a new namespace",
1075
+ (yargs) => {
1076
+ return yargs
1077
+ .positional("namespace", {
1078
+ describe: "The name of the new namespace",
1079
+ type: "string",
1080
+ demandOption: true,
1081
+ })
1082
+ .option("env", {
1083
+ type: "string",
1084
+ requiresArg: true,
1085
+ describe: "Perform on a specific environment",
1086
+ alias: "e",
1087
+ })
1088
+ .option("preview", {
1089
+ type: "boolean",
1090
+ describe: "Interact with a preview namespace",
1091
+ });
1092
+ },
1093
+ async (args) => {
1094
+ await printWranglerBanner();
1095
+
1096
+ if (!isValidKVNamespaceBinding(args.namespace)) {
1097
+ throw new CommandLineArgsError(
1098
+ `The namespace binding name "${args.namespace}" is invalid. It can only have alphanumeric and _ characters, and cannot begin with a number.`
1099
+ );
1100
+ }
1101
+
1102
+ const config = readConfig(args.config as ConfigPath, args);
1103
+ if (!config.name) {
1104
+ logger.warn(
1105
+ "No configured name present, using `worker` as a prefix for the title"
1106
+ );
1107
+ }
1108
+
1109
+ const name = config.name || "worker";
1110
+ const environment = args.env ? `-${args.env}` : "";
1111
+ const preview = args.preview ? "_preview" : "";
1112
+ const title = `${name}${environment}-${args.namespace}${preview}`;
1113
+
1114
+ const accountId = await requireAuth(config);
1115
+
1116
+ // TODO: generate a binding name stripping non alphanumeric chars
1117
+
1118
+ logger.log(`🌀 Creating namespace with title "${title}"`);
1119
+ const namespaceId = await createKVNamespace(accountId, title);
1120
+
1121
+ logger.log("✨ Success!");
1122
+ const envString = args.env ? ` under [env.${args.env}]` : "";
1123
+ const previewString = args.preview ? "preview_" : "";
1124
+ logger.log(
1125
+ `Add the following to your configuration file in your kv_namespaces array${envString}:`
1126
+ );
1127
+ logger.log(
1128
+ `{ binding = "${args.namespace}", ${previewString}id = "${namespaceId}" }`
1129
+ );
1130
+
1131
+ // TODO: automatically write this block to the wrangler.toml config file??
1132
+ }
1133
+ )
1134
+ .command(
1135
+ "list",
1136
+ "Outputs a list of all KV namespaces associated with your account id.",
1137
+ {},
1138
+ async (args) => {
1139
+ const config = readConfig(args.config as ConfigPath, args);
1140
+
1141
+ const accountId = await requireAuth(config);
1142
+
1143
+ // TODO: we should show bindings if they exist for given ids
1144
+
1145
+ logger.log(
1146
+ JSON.stringify(await listKVNamespaces(accountId), null, " ")
1147
+ );
1148
+ }
1149
+ )
1150
+ .command(
1151
+ "delete",
1152
+ "Deletes a given namespace.",
1153
+ (yargs) => {
1154
+ return yargs
1155
+ .option("binding", {
1156
+ type: "string",
1157
+ requiresArg: true,
1158
+ describe: "The name of the namespace to delete",
1159
+ })
1160
+ .option("namespace-id", {
1161
+ type: "string",
1162
+ requiresArg: true,
1163
+ describe: "The id of the namespace to delete",
1164
+ })
1165
+ .check(demandOneOfOption("binding", "namespace-id"))
1166
+ .option("env", {
1167
+ type: "string",
1168
+ requiresArg: true,
1169
+ describe: "Perform on a specific environment",
1170
+ alias: "e",
1171
+ })
1172
+ .option("preview", {
1173
+ type: "boolean",
1174
+ describe: "Interact with a preview namespace",
1175
+ });
1176
+ },
1177
+ async (args) => {
1178
+ await printWranglerBanner();
1179
+ const config = readConfig(args.config as ConfigPath, args);
1180
+
1181
+ let id;
1182
+ try {
1183
+ id = getKVNamespaceId(args, config);
1184
+ } catch (e) {
1185
+ throw new CommandLineArgsError(
1186
+ "Not able to delete namespace.\n" + ((e as Error).message ?? e)
1187
+ );
1188
+ }
1189
+
1190
+ const accountId = await requireAuth(config);
1191
+
1192
+ await deleteKVNamespace(accountId, id);
1193
+
1194
+ // TODO: recommend they remove it from wrangler.toml
1195
+
1196
+ // test-mf wrangler kv:namespace delete --namespace-id 2a7d3d8b23fc4159b5afa489d6cfd388
1197
+ // Are you sure you want to delete namespace 2a7d3d8b23fc4159b5afa489d6cfd388? [y/n]
1198
+ // n
1199
+ // 💁 Not deleting namespace 2a7d3d8b23fc4159b5afa489d6cfd388
1200
+ // ➜ test-mf wrangler kv:namespace delete --namespace-id 2a7d3d8b23fc4159b5afa489d6cfd388
1201
+ // Are you sure you want to delete namespace 2a7d3d8b23fc4159b5afa489d6cfd388? [y/n]
1202
+ // y
1203
+ // 🌀 Deleting namespace 2a7d3d8b23fc4159b5afa489d6cfd388
1204
+ // ✨ Success
1205
+ // ⚠️ Make sure to remove this "kv-namespace" entry from your configuration file!
1206
+ // ➜ test-mf
1207
+
1208
+ // TODO: do it automatically
1209
+
1210
+ // TODO: delete the preview namespace as well?
1211
+ }
1212
+ );
1213
+ }
1214
+ );
1215
+
1216
+ // :key
1217
+ wrangler.command(
1218
+ "kv:key",
1219
+ "🔑 Individually manage Workers KV key-value pairs",
1220
+ (kvKeyYargs) => {
1221
+ return kvKeyYargs
1222
+ .command(subHelp)
1223
+ .command(
1224
+ "put <key> [value]",
1225
+ "Writes a single key/value pair to the given namespace.",
1226
+ (yargs) => {
1227
+ return yargs
1228
+ .positional("key", {
1229
+ type: "string",
1230
+ describe: "The key to write to",
1231
+ demandOption: true,
1232
+ })
1233
+ .positional("value", {
1234
+ type: "string",
1235
+ describe: "The value to write",
1236
+ })
1237
+ .option("binding", {
1238
+ type: "string",
1239
+ requiresArg: true,
1240
+ describe: "The binding of the namespace to write to",
1241
+ })
1242
+ .option("namespace-id", {
1243
+ type: "string",
1244
+ requiresArg: true,
1245
+ describe: "The id of the namespace to write to",
1246
+ })
1247
+ .check(demandOneOfOption("binding", "namespace-id"))
1248
+ .option("env", {
1249
+ type: "string",
1250
+ requiresArg: true,
1251
+ describe: "Perform on a specific environment",
1252
+ alias: "e",
1253
+ })
1254
+ .option("preview", {
1255
+ type: "boolean",
1256
+ describe: "Interact with a preview namespace",
1257
+ })
1258
+ .option("ttl", {
1259
+ type: "number",
1260
+ describe: "Time for which the entries should be visible",
1261
+ })
1262
+ .option("expiration", {
1263
+ type: "number",
1264
+ describe:
1265
+ "Time since the UNIX epoch after which the entry expires",
1266
+ })
1267
+ .option("metadata", {
1268
+ type: "string",
1269
+ describe: "Arbitrary JSON that is associated with a key",
1270
+ coerce: (jsonStr: string): KeyValue["metadata"] => {
1271
+ try {
1272
+ return JSON.parse(jsonStr);
1273
+ } catch (_) {}
1274
+ },
1275
+ })
1276
+ .option("path", {
1277
+ type: "string",
1278
+ requiresArg: true,
1279
+ describe: "Read value from the file at a given path",
1280
+ })
1281
+ .check(demandOneOfOption("value", "path"));
1282
+ },
1283
+ async ({ key, ttl, expiration, metadata, ...args }) => {
1284
+ await printWranglerBanner();
1285
+ const config = readConfig(args.config as ConfigPath, args);
1286
+ const namespaceId = getKVNamespaceId(args, config);
1287
+ // One of `args.path` and `args.value` must be defined
1288
+ const value = args.path
1289
+ ? readFileSyncToBuffer(args.path)
1290
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1291
+ args.value!;
1292
+
1293
+ const metadataLog = metadata
1294
+ ? ` with metadata "${JSON.stringify(metadata)}"`
1295
+ : "";
1296
+
1297
+ if (args.path) {
1298
+ logger.log(
1299
+ `Writing the contents of ${args.path} to the key "${key}" on namespace ${namespaceId}${metadataLog}.`
1300
+ );
1301
+ } else {
1302
+ logger.log(
1303
+ `Writing the value "${value}" to key "${key}" on namespace ${namespaceId}${metadataLog}.`
1304
+ );
1305
+ }
1306
+
1307
+ const accountId = await requireAuth(config);
1308
+
1309
+ await putKVKeyValue(accountId, namespaceId, {
1310
+ key,
1311
+ value,
1312
+ expiration,
1313
+ expiration_ttl: ttl,
1314
+ metadata: metadata as KeyValue["metadata"],
1315
+ });
1316
+ }
1317
+ )
1318
+ .command(
1319
+ "list",
1320
+ "Outputs a list of all keys in a given namespace.",
1321
+ (yargs) => {
1322
+ return yargs
1323
+ .option("binding", {
1324
+ type: "string",
1325
+ requiresArg: true,
1326
+ describe: "The name of the namespace to list",
1327
+ })
1328
+ .option("namespace-id", {
1329
+ type: "string",
1330
+ requiresArg: true,
1331
+ describe: "The id of the namespace to list",
1332
+ })
1333
+ .check(demandOneOfOption("binding", "namespace-id"))
1334
+ .option("env", {
1335
+ type: "string",
1336
+ requiresArg: true,
1337
+ describe: "Perform on a specific environment",
1338
+ alias: "e",
1339
+ })
1340
+ .option("preview", {
1341
+ type: "boolean",
1342
+ // In the case of listing keys we will default to non-preview mode
1343
+ default: false,
1344
+ describe: "Interact with a preview namespace",
1345
+ })
1346
+ .option("prefix", {
1347
+ type: "string",
1348
+ requiresArg: true,
1349
+ describe: "A prefix to filter listed keys",
1350
+ });
1351
+ },
1352
+ async ({ prefix, ...args }) => {
1353
+ // TODO: support for limit+cursor (pagination)
1354
+ const config = readConfig(args.config as ConfigPath, args);
1355
+ const namespaceId = getKVNamespaceId(args, config);
1356
+
1357
+ const accountId = await requireAuth(config);
1358
+
1359
+ const results = await listKVNamespaceKeys(
1360
+ accountId,
1361
+ namespaceId,
1362
+ prefix
1363
+ );
1364
+ logger.log(JSON.stringify(results, undefined, 2));
1365
+ }
1366
+ )
1367
+ .command(
1368
+ "get <key>",
1369
+ "Reads a single value by key from the given namespace.",
1370
+ (yargs) => {
1371
+ return yargs
1372
+ .positional("key", {
1373
+ describe: "The key value to get.",
1374
+ type: "string",
1375
+ demandOption: true,
1376
+ })
1377
+ .option("binding", {
1378
+ type: "string",
1379
+ requiresArg: true,
1380
+ describe: "The name of the namespace to get from",
1381
+ })
1382
+ .option("namespace-id", {
1383
+ type: "string",
1384
+ requiresArg: true,
1385
+ describe: "The id of the namespace to get from",
1386
+ })
1387
+ .check(demandOneOfOption("binding", "namespace-id"))
1388
+ .option("env", {
1389
+ type: "string",
1390
+ requiresArg: true,
1391
+ describe: "Perform on a specific environment",
1392
+ alias: "e",
1393
+ })
1394
+ .option("preview", {
1395
+ type: "boolean",
1396
+ describe: "Interact with a preview namespace",
1397
+ })
1398
+ .option("preview", {
1399
+ type: "boolean",
1400
+ // In the case of getting key values we will default to non-preview mode
1401
+ default: false,
1402
+ describe: "Interact with a preview namespace",
1403
+ })
1404
+ .option("text", {
1405
+ type: "boolean",
1406
+ default: false,
1407
+ describe: "Decode the returned value as a utf8 string",
1408
+ });
1409
+ },
1410
+ async ({ key, ...args }) => {
1411
+ const config = readConfig(args.config as ConfigPath, args);
1412
+ const namespaceId = getKVNamespaceId(args, config);
1413
+
1414
+ const accountId = await requireAuth(config);
1415
+ const bufferKVValue = Buffer.from(
1416
+ await getKVKeyValue(accountId, namespaceId, key)
1417
+ );
1418
+
1419
+ if (args.text) {
1420
+ const decoder = new StringDecoder("utf8");
1421
+ logger.log(decoder.write(bufferKVValue));
1422
+ } else {
1423
+ process.stdout.write(bufferKVValue);
1424
+ }
1425
+ }
1426
+ )
1427
+ .command(
1428
+ "delete <key>",
1429
+ "Removes a single key value pair from the given namespace.",
1430
+ (yargs) => {
1431
+ return yargs
1432
+ .positional("key", {
1433
+ describe: "The key value to delete",
1434
+ type: "string",
1435
+ demandOption: true,
1436
+ })
1437
+ .option("binding", {
1438
+ type: "string",
1439
+ requiresArg: true,
1440
+ describe: "The name of the namespace to delete from",
1441
+ })
1442
+ .option("namespace-id", {
1443
+ type: "string",
1444
+ requiresArg: true,
1445
+ describe: "The id of the namespace to delete from",
1446
+ })
1447
+ .check(demandOneOfOption("binding", "namespace-id"))
1448
+ .option("env", {
1449
+ type: "string",
1450
+ requiresArg: true,
1451
+ describe: "Perform on a specific environment",
1452
+ alias: "e",
1453
+ })
1454
+ .option("preview", {
1455
+ type: "boolean",
1456
+ describe: "Interact with a preview namespace",
1457
+ });
1458
+ },
1459
+ async ({ key, ...args }) => {
1460
+ await printWranglerBanner();
1461
+ const config = readConfig(args.config as ConfigPath, args);
1462
+ const namespaceId = getKVNamespaceId(args, config);
1463
+
1464
+ logger.log(
1465
+ `Deleting the key "${key}" on namespace ${namespaceId}.`
1466
+ );
1467
+
1468
+ const accountId = await requireAuth(config);
1469
+
1470
+ await deleteKVKeyValue(accountId, namespaceId, key);
1471
+ }
1472
+ );
1473
+ }
1474
+ );
1475
+
1476
+ // :bulk
1477
+ wrangler.command(
1478
+ "kv:bulk",
1479
+ "💪 Interact with multiple Workers KV key-value pairs at once",
1480
+ (kvBulkYargs) => {
1481
+ return kvBulkYargs
1482
+ .command(subHelp)
1483
+ .command(
1484
+ "put <filename>",
1485
+ "Upload multiple key-value pairs to a namespace",
1486
+ (yargs) => {
1487
+ return yargs
1488
+ .positional("filename", {
1489
+ describe: `The JSON file of key-value pairs to upload, in form [{"key":..., "value":...}"...]`,
1490
+ type: "string",
1491
+ demandOption: true,
1492
+ })
1493
+ .option("binding", {
1494
+ type: "string",
1495
+ requiresArg: true,
1496
+ describe: "The name of the namespace to insert values into",
1497
+ })
1498
+ .option("namespace-id", {
1499
+ type: "string",
1500
+ requiresArg: true,
1501
+ describe: "The id of the namespace to insert values into",
1502
+ })
1503
+ .check(demandOneOfOption("binding", "namespace-id"))
1504
+ .option("env", {
1505
+ type: "string",
1506
+ requiresArg: true,
1507
+ describe: "Perform on a specific environment",
1508
+ alias: "e",
1509
+ })
1510
+ .option("preview", {
1511
+ type: "boolean",
1512
+ describe: "Interact with a preview namespace",
1513
+ });
1514
+ },
1515
+ async ({ filename, ...args }) => {
1516
+ await printWranglerBanner();
1517
+ // The simplest implementation I could think of.
1518
+ // This could be made more efficient with a streaming parser/uploader
1519
+ // but we'll do that in the future if needed.
1520
+
1521
+ const config = readConfig(args.config as ConfigPath, args);
1522
+ const namespaceId = getKVNamespaceId(args, config);
1523
+ const content = parseJSON(readFileSync(filename), filename);
1524
+
1525
+ if (!Array.isArray(content)) {
1526
+ throw new Error(
1527
+ `Unexpected JSON input from "${filename}".\n` +
1528
+ `Expected an array of key-value objects but got type "${typeof content}".`
1529
+ );
1530
+ }
1531
+
1532
+ const errors: string[] = [];
1533
+ const warnings: string[] = [];
1534
+ for (let i = 0; i < content.length; i++) {
1535
+ const keyValue = content[i];
1536
+ if (!isKVKeyValue(keyValue)) {
1537
+ errors.push(
1538
+ `The item at index ${i} is ${JSON.stringify(keyValue)}`
1539
+ );
1540
+ } else {
1541
+ const props = unexpectedKVKeyValueProps(keyValue);
1542
+ if (props.length > 0) {
1543
+ warnings.push(
1544
+ `The item at index ${i} contains unexpected properties: ${JSON.stringify(
1545
+ props
1546
+ )}.`
1547
+ );
1548
+ }
1549
+ }
1550
+ }
1551
+ if (warnings.length > 0) {
1552
+ logger.warn(
1553
+ `Unexpected key-value properties in "${filename}".\n` +
1554
+ warnings.join("\n")
1555
+ );
1556
+ }
1557
+ if (errors.length > 0) {
1558
+ throw new Error(
1559
+ `Unexpected JSON input from "${filename}".\n` +
1560
+ `Each item in the array should be an object that matches:\n\n` +
1561
+ `interface KeyValue {\n` +
1562
+ ` key: string;\n` +
1563
+ ` value: string;\n` +
1564
+ ` expiration?: number;\n` +
1565
+ ` expiration_ttl?: number;\n` +
1566
+ ` metadata?: object;\n` +
1567
+ ` base64?: boolean;\n` +
1568
+ `}\n\n` +
1569
+ errors.join("\n")
1570
+ );
1571
+ }
1572
+
1573
+ const accountId = await requireAuth(config);
1574
+ await putKVBulkKeyValue(accountId, namespaceId, content);
1575
+
1576
+ logger.log("Success!");
1577
+ }
1578
+ )
1579
+ .command(
1580
+ "delete <filename>",
1581
+ "Delete multiple key-value pairs from a namespace",
1582
+ (yargs) => {
1583
+ return yargs
1584
+ .positional("filename", {
1585
+ describe: `The JSON file of keys to delete, in the form ["key1", "key2", ...]`,
1586
+ type: "string",
1587
+ demandOption: true,
1588
+ })
1589
+ .option("binding", {
1590
+ type: "string",
1591
+ requiresArg: true,
1592
+ describe: "The name of the namespace to delete from",
1593
+ })
1594
+ .option("namespace-id", {
1595
+ type: "string",
1596
+ requiresArg: true,
1597
+ describe: "The id of the namespace to delete from",
1598
+ })
1599
+ .check(demandOneOfOption("binding", "namespace-id"))
1600
+ .option("env", {
1601
+ type: "string",
1602
+ requiresArg: true,
1603
+ describe: "Perform on a specific environment",
1604
+ alias: "e",
1605
+ })
1606
+ .option("preview", {
1607
+ type: "boolean",
1608
+ describe: "Interact with a preview namespace",
1609
+ })
1610
+ .option("force", {
1611
+ type: "boolean",
1612
+ alias: "f",
1613
+ describe: "Do not ask for confirmation before deleting",
1614
+ });
1615
+ },
1616
+ async ({ filename, ...args }) => {
1617
+ await printWranglerBanner();
1618
+ const config = readConfig(args.config as ConfigPath, args);
1619
+ const namespaceId = getKVNamespaceId(args, config);
1620
+
1621
+ if (!args.force) {
1622
+ const result = await confirm(
1623
+ `Are you sure you want to delete all the keys read from "${filename}" from kv-namespace with id "${namespaceId}"?`
1624
+ );
1625
+ if (!result) {
1626
+ logger.log(`Not deleting keys read from "${filename}".`);
1627
+ return;
1628
+ }
1629
+ }
1630
+
1631
+ const content = parseJSON(
1632
+ readFileSync(filename),
1633
+ filename
1634
+ ) as string[];
1635
+
1636
+ if (!Array.isArray(content)) {
1637
+ throw new Error(
1638
+ `Unexpected JSON input from "${filename}".\n` +
1639
+ `Expected an array of strings but got:\n${content}`
1640
+ );
1641
+ }
1642
+
1643
+ const errors: string[] = [];
1644
+ for (let i = 0; i < content.length; i++) {
1645
+ const key = content[i];
1646
+ if (typeof key !== "string") {
1647
+ errors.push(
1648
+ `The item at index ${i} is type: "${typeof key}" - ${JSON.stringify(
1649
+ key
1650
+ )}`
1651
+ );
1652
+ }
1653
+ }
1654
+ if (errors.length > 0) {
1655
+ throw new Error(
1656
+ `Unexpected JSON input from "${filename}".\n` +
1657
+ `Expected an array of strings.\n` +
1658
+ errors.join("\n")
1659
+ );
1660
+ }
1661
+
1662
+ const accountId = await requireAuth(config);
1663
+
1664
+ await deleteKVBulkKeyValue(accountId, namespaceId, content);
1665
+
1666
+ logger.log("Success!");
1667
+ }
1668
+ );
1669
+ }
1670
+ );
1671
+
1672
+ wrangler.command(
1673
+ "pages",
1674
+ "⚡️ Configure Cloudflare Pages",
1675
+ async (pagesYargs) => {
1676
+ await pages(pagesYargs.command(subHelp));
1677
+ }
1678
+ );
1679
+
1680
+ wrangler.command("r2", "📦 Interact with an R2 store", (r2Yargs) => {
1681
+ return r2Yargs
1682
+ .command(subHelp)
1683
+ .command("bucket", "Manage R2 buckets", (r2BucketYargs) => {
1684
+ r2BucketYargs.command(
1685
+ "create <name>",
1686
+ "Create a new R2 bucket",
1687
+ (yargs) => {
1688
+ return yargs.positional("name", {
1689
+ describe: "The name of the new bucket",
1690
+ type: "string",
1691
+ demandOption: true,
1692
+ });
1693
+ },
1694
+ async (args) => {
1695
+ await printWranglerBanner();
1696
+
1697
+ const config = readConfig(args.config as ConfigPath, args);
1698
+
1699
+ const accountId = await requireAuth(config);
1700
+
1701
+ logger.log(`Creating bucket ${args.name}.`);
1702
+ await createR2Bucket(accountId, args.name);
1703
+ logger.log(`Created bucket ${args.name}.`);
1704
+ }
1705
+ );
1706
+
1707
+ r2BucketYargs.command("list", "List R2 buckets", {}, async (args) => {
1708
+ const config = readConfig(args.config as ConfigPath, args);
1709
+
1710
+ const accountId = await requireAuth(config);
1711
+
1712
+ logger.log(JSON.stringify(await listR2Buckets(accountId), null, 2));
1713
+ });
1714
+
1715
+ r2BucketYargs.command(
1716
+ "delete <name>",
1717
+ "Delete an R2 bucket",
1718
+ (yargs) => {
1719
+ return yargs.positional("name", {
1720
+ describe: "The name of the bucket to delete",
1721
+ type: "string",
1722
+ demandOption: true,
1723
+ });
1724
+ },
1725
+ async (args) => {
1726
+ await printWranglerBanner();
1727
+
1728
+ const config = readConfig(args.config as ConfigPath, args);
1729
+
1730
+ const accountId = await requireAuth(config);
1731
+
1732
+ logger.log(`Deleting bucket ${args.name}.`);
1733
+ await deleteR2Bucket(accountId, args.name);
1734
+ logger.log(`Deleted bucket ${args.name}.`);
1735
+ }
1736
+ );
1737
+ return r2BucketYargs;
1738
+ });
1739
+ });
1740
+
1741
+ wrangler.command(
1742
+ "worker-namespace",
1743
+ "📦 Interact with a worker namespace",
1744
+ (workerNamespaceYargs) => {
1745
+ return workerNamespaceCommands(workerNamespaceYargs, subHelp);
1746
+ }
1747
+ );
1748
+
1749
+ wrangler.command(
1750
+ "pubsub",
1751
+ "📮 Interact and manage Pub/Sub Brokers",
1752
+ (pubsubYargs) => {
1753
+ return pubSubCommands(pubsubYargs, subHelp);
1754
+ }
1755
+ );
1756
+
1757
+ /**
1758
+ * User Group: login, logout, and whoami
1759
+ * TODO: group commands into User group similar to .group() for flags in yargs
1760
+ */
1761
+ // login
1762
+ wrangler.command(
1763
+ // this needs scopes as an option?
1764
+ "login",
1765
+ "🔓 Login to Cloudflare",
1766
+ (yargs) => {
1767
+ // TODO: This needs some copy editing
1768
+ // I mean, this entire app does, but this too.
1769
+ return yargs
1770
+ .option("scopes-list", {
1771
+ describe: "List all the available OAuth scopes with descriptions",
1772
+ })
1773
+ .option("scopes", {
1774
+ describe: "Pick the set of applicable OAuth scopes when logging in",
1775
+ array: true,
1776
+ type: "string",
1777
+ requiresArg: true,
1778
+ });
1779
+
1780
+ // TODO: scopes
1781
+ },
1782
+ async (args) => {
1783
+ await printWranglerBanner();
1784
+ if (args["scopes-list"]) {
1785
+ listScopes();
1786
+ return;
1787
+ }
1788
+ if (args.scopes) {
1789
+ if (args.scopes.length === 0) {
1790
+ // don't allow no scopes to be passed, that would be weird
1791
+ listScopes();
1792
+ return;
1793
+ }
1794
+ if (!validateScopeKeys(args.scopes)) {
1795
+ throw new CommandLineArgsError(
1796
+ `One of ${args.scopes} is not a valid authentication scope. Run "wrangler login --list-scopes" to see the valid scopes.`
1797
+ );
1798
+ }
1799
+ await login({ scopes: args.scopes });
1800
+ return;
1801
+ }
1802
+ await login();
1803
+
1804
+ // TODO: would be nice if it optionally saved login
1805
+ // credentials inside node_modules/.cache or something
1806
+ // this way you could have multiple users on a single machine
1807
+ }
1808
+ );
1809
+
1810
+ // logout
1811
+ wrangler.command(
1812
+ // this needs scopes as an option?
1813
+ "logout",
1814
+ "🚪 Logout from Cloudflare",
1815
+ () => {},
1816
+ async () => {
1817
+ await printWranglerBanner();
1818
+ await logout();
1819
+ }
1820
+ );
1821
+
1822
+ // whoami
1823
+ wrangler.command(
1824
+ "whoami",
1825
+ "🕵️ Retrieve your user info and test your auth config",
1826
+ () => {},
1827
+ async () => {
1828
+ await printWranglerBanner();
1829
+ await whoami();
1830
+ }
1831
+ );
1832
+
1833
+ wrangler.option("config", {
1834
+ alias: "c",
1835
+ describe: "Path to .toml configuration file",
1836
+ type: "string",
1837
+ requiresArg: true,
1838
+ });
1839
+
1840
+ wrangler.group(["config", "help", "version"], "Flags:");
1841
+ wrangler.help().alias("h", "help");
1842
+ wrangler.version(wranglerVersion).alias("v", "version");
1843
+ wrangler.exitProcess(false);
1844
+
1845
+ return wrangler;
1846
+ }
108
1847
 
109
1848
  export async function main(argv: string[]): Promise<void> {
110
- const wrangler = makeCLI(argv)
111
- // We handle errors ourselves in a try-catch around `yargs.parse`.
112
- // If you want the "help info" to be displayed then throw an instance of `CommandLineArgsError`.
113
- // Otherwise we just log the error that was thrown without any "help info".
114
- .showHelpOnFail(false)
115
- .fail((msg, error) => {
116
- if (!error) {
117
- // If there is only a `msg` then this came from yargs own validation, so wrap in a `CommandLineArgsError`.
118
- error = new CommandLineArgsError(msg);
119
- }
120
- throw error;
121
- })
122
- .scriptName("wrangler")
123
- .wrap(null);
124
-
125
- // the default is to simply print the help menu
126
- wrangler.command(
127
- ["*"],
128
- false,
129
- () => {},
130
- (args) => {
131
- if (args._.length > 0) {
132
- throw new CommandLineArgsError(`Unknown command: ${args._}.`);
133
- } else {
134
- wrangler.showHelp("log");
135
- }
136
- }
137
- );
138
-
139
- // You will note that we use the form for all commands where we use the builder function
140
- // to define options and subcommands.
141
- // Further we return the result of this builder even though it's not completely necessary.
142
- // The reason is that it's required for type inference of the args in the handle function.
143
- // I wish we could enforce this pattern, but this comment will have to do for now.
144
- // (It's also annoying that choices[] doesn't get inferred as an enum. 🤷‍♂.)
145
-
146
- // [DEPRECATED] generate
147
- wrangler.command(
148
- // we definitely want to move away from us cloning github templates
149
- // we can do something better here, let's see
150
- "generate [name] [template]",
151
- false,
152
- (yargs) => {
153
- return yargs
154
- .positional("name", {
155
- describe: "Name of the Workers project",
156
- default: "worker",
157
- })
158
- .positional("template", {
159
- describe: "a link to a GitHub template",
160
- default: "https://github.com/cloudflare/worker-template",
161
- });
162
- },
163
- () => {
164
- // "👯 [DEPRECATED]. Scaffold a Cloudflare Workers project from a public GitHub repository.",
165
- throw new DeprecationError(
166
- "`wrangler generate` has been deprecated, please refer to TODO://some/path for alternatives"
167
- );
168
- }
169
- );
170
-
171
- // init
172
- wrangler.command(
173
- "init [name]",
174
- "📥 Create a wrangler.toml configuration file",
175
- (yargs) => {
176
- return yargs.positional("name", {
177
- describe: "The name of your worker.",
178
- type: "string",
179
- });
180
- },
181
- async (args) => {
182
- if ("type" in args) {
183
- let message = "The --type option is no longer supported.";
184
- if (args.type === "webpack") {
185
- message +=
186
- "\nIf you wish to use webpack then you will need to create a custom build.";
187
- // TODO: Add a link to docs
188
- }
189
- throw new CommandLineArgsError(message);
190
- }
191
-
192
- const destination = path.join(process.cwd(), "wrangler.toml");
193
- if (fs.existsSync(destination)) {
194
- console.warn(`${destination} file already exists!`);
195
- const result = await confirm(
196
- "Do you want to continue initializing this project?"
197
- );
198
- if (!result) {
199
- return;
200
- }
201
- }
202
-
203
- const compatibilityDate = new Date().toISOString().substring(0, 10);
204
- try {
205
- await writeFile(
206
- destination,
207
- `compatibility_date = "${compatibilityDate}"` + "\n"
208
- );
209
- console.log(`✨ Successfully created wrangler.toml`);
210
- // TODO: suggest next steps?
211
- } catch (err) {
212
- throw new Error(
213
- `Failed to create wrangler.toml.\n${err.message ?? err}`
214
- );
215
- }
216
-
217
- // if no package.json, ask, and if yes, create one
218
- let pathToPackageJson = await findUp("package.json");
219
-
220
- if (!pathToPackageJson) {
221
- if (
222
- await confirm("No package.json found. Would you like to create one?")
223
- ) {
224
- await writeFile(
225
- path.join(process.cwd(), "package.json"),
226
- JSON.stringify(
227
- {
228
- name: "worker",
229
- version: "0.0.1",
230
- },
231
- null,
232
- " "
233
- ) + "\n"
234
- );
235
- console.log(`✨ Created package.json`);
236
- pathToPackageJson = path.join(process.cwd(), "package.json");
237
- } else {
238
- return;
239
- }
240
- }
241
-
242
- // if workers-types doesn't exist as a dependency
243
- // offer to install it
244
- // and make a tsconfig?
245
- let pathToTSConfig = await findUp("tsconfig.json");
246
- if (!pathToTSConfig) {
247
- if (await confirm("Would you like to use typescript?")) {
248
- await writeFile(
249
- path.join(process.cwd(), "tsconfig.json"),
250
- JSON.stringify(
251
- {
252
- compilerOptions: {
253
- target: "esnext",
254
- module: "esnext",
255
- moduleResolution: "node",
256
- esModuleInterop: true,
257
- allowJs: true,
258
- allowSyntheticDefaultImports: true,
259
- isolatedModules: true,
260
- noEmit: true,
261
- lib: ["esnext"],
262
- jsx: "react",
263
- resolveJsonModule: true,
264
- types: ["@cloudflare/workers-types"],
265
- },
266
- },
267
- null,
268
- " "
269
- ) + "\n"
270
- );
271
- await execa("npm", [
272
- "install",
273
- "@cloudflare/workers-types",
274
- "--save-dev",
275
- ]);
276
- console.log(
277
- `✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies`
278
- );
279
- pathToTSConfig = path.join(process.cwd(), "tsconfig.json");
280
- }
281
- }
282
- }
283
- );
284
-
285
- // build
286
- wrangler.command(
287
- "build",
288
- false,
289
- (yargs) => {
290
- return yargs.option("env", {
291
- describe: "Perform on a specific environment",
292
- });
293
- },
294
- () => {
295
- // "[DEPRECATED] 🦀 Build your project (if applicable)",
296
- throw new DeprecationError(
297
- "`wrangler build` has been deprecated, please refer to TODO://some/path for alternatives"
298
- );
299
- }
300
- );
301
-
302
- // login
303
- wrangler.command(
304
- // this needs scopes as an option?
305
- "login",
306
- false, // we don't need to show this in the menu
307
- // "🔓 Login to Cloudflare",
308
- (yargs) => {
309
- // TODO: This needs some copy editing
310
- // I mean, this entire app does, but this too.
311
- return yargs
312
- .option("scopes-list", {
313
- describe: "list all the available OAuth scopes with descriptions.",
314
- })
315
- .option("scopes", {
316
- describe: "allows to choose your set of OAuth scopes.",
317
- array: true,
318
- type: "string",
319
- });
320
-
321
- // TODO: scopes
322
- },
323
- async (args) => {
324
- if (args["scopes-list"]) {
325
- listScopes();
326
- return;
327
- }
328
- if (args.scopes) {
329
- if (args.scopes.length === 0) {
330
- // don't allow no scopes to be passed, that would be weird
331
- listScopes();
332
- return;
333
- }
334
- await login({ scopes: args.scopes });
335
- return;
336
- }
337
- await login();
338
-
339
- // TODO: would be nice if it optionally saved login
340
- // credentials inside node_modules/.cache or something
341
- // this way you could have multiple users on a single machine
342
- }
343
- );
344
-
345
- // logout
346
- wrangler.command(
347
- // this needs scopes as an option?
348
- "logout",
349
- false, // we don't need to show this in the menu
350
- // "🚪 Logout from Cloudflare",
351
- () => {},
352
- async () => {
353
- await logout();
354
- }
355
- );
356
-
357
- // whoami
358
- wrangler.command(
359
- "whoami",
360
- false, // we don't need to show this the menu
361
- // "🕵️ Retrieve your user info and test your auth config",
362
- () => {},
363
- (args) => {
364
- console.log(":whoami", args);
365
- }
366
- );
367
-
368
- // config
369
- wrangler.command(
370
- "config",
371
- false,
372
- () => {},
373
- () => {
374
- // "🕵️ Authenticate Wrangler with a Cloudflare API Token",
375
- throw new DeprecationError(
376
- "`wrangler config` has been deprecated, please refer to TODO://some/path for alternatives"
377
- );
378
- }
379
- );
380
-
381
- // dev
382
- wrangler.command(
383
- "dev <filename>",
384
- "👂 Start a local server for developing your worker",
385
- (yargs) => {
386
- return yargs
387
- .positional("filename", {
388
- describe: "entry point",
389
- type: "string",
390
- demandOption: true,
391
- })
392
- .option("name", {
393
- describe: "name of the script",
394
- type: "string",
395
- })
396
- .option("format", {
397
- choices: ["modules", "service-worker"] as const,
398
- describe: "Choose an entry type",
399
- })
400
- .option("env", {
401
- describe: "Perform on a specific environment",
402
- type: "string",
403
- // TODO: get choices for the toml file?
404
- })
405
- .option("compatibility-date", {
406
- describe: "Date to use for compatibility checks",
407
- type: "string",
408
- })
409
- .option("compatibility-flags", {
410
- describe: "Flags to use for compatibility checks",
411
- type: "array",
412
- alias: "compatibility-flag",
413
- })
414
- .option("latest", {
415
- describe: "Use the latest version of the worker runtime",
416
- type: "boolean",
417
- default: true,
418
- })
419
- .option("ip", {
420
- describe: "IP address to listen on",
421
- type: "string",
422
- default: "127.0.0.1",
423
- })
424
- .option("port", {
425
- describe: "Port to listen on, defaults to 8787",
426
- type: "number",
427
- default: 8787,
428
- })
429
- .option("host", {
430
- type: "string",
431
- describe:
432
- "Host to forward requests to, defaults to the zone of project",
433
- })
434
- .option("local-protocol", {
435
- default: "http",
436
- describe: "Protocol to listen to requests on, defaults to http.",
437
- choices: ["http", "https"],
438
- })
439
- .option("experimental-public", {
440
- describe: "Static assets to be served",
441
- type: "string",
442
- })
443
- .option("site", {
444
- describe: "Root folder of static assets for Workers Sites",
445
- type: "string",
446
- })
447
- .option("upstream-protocol", {
448
- default: "https",
449
- describe:
450
- "Protocol to forward requests to host on, defaults to https.",
451
- choices: ["http", "https"],
452
- })
453
- .option("jsx-factory", {
454
- describe: "The function that is called for each JSX element",
455
- type: "string",
456
- })
457
- .option("jsx-fragment", {
458
- describe: "The function that is called for each JSX fragment",
459
- type: "string",
460
- });
461
- },
462
- async (args) => {
463
- const { filename, format } = args;
464
- const config = args.config as Config;
465
-
466
- if (args["experimental-public"]) {
467
- console.warn(
468
- "🚨 The --experimental-public field is experimental and will change in the future."
469
- );
470
- }
471
-
472
- if (args.public) {
473
- throw new Error(
474
- "🚨 The --public field has been renamed to --experimental-public, and will change behaviour in the future."
475
- );
476
- }
477
-
478
- if (!args.local) {
479
- // -- snip, extract --
480
- const loggedIn = await loginOrRefreshIfRequired();
481
- if (!loggedIn) {
482
- // didn't login, let's just quit
483
- console.log("Did not login, quitting...");
484
- return;
485
- }
486
-
487
- if (!config.account_id) {
488
- config.account_id = await getAccountId();
489
- if (!config.account_id) {
490
- throw new Error("No account id found, quitting...");
491
- }
492
- }
493
- // -- snip, end --
494
- }
495
-
496
- const environments = config.env ?? {};
497
- const envRootObj = args.env ? environments[args.env] || {} : config;
498
-
499
- // TODO: this error shouldn't actually happen,
500
- // but we haven't fixed it internally yet
501
- if ("durable_objects" in envRootObj) {
502
- if (!(args.name || config.name)) {
503
- console.warn(
504
- 'A worker with durable objects needs to be named, or it may not work as expected. Add a "name" into wrangler.toml, or pass it in the command line with --name.'
505
- );
506
- }
507
- // TODO: if not already published, publish a draft worker
508
- }
509
-
510
- const { waitUntilExit } = render(
511
- <Dev
512
- name={args.name || config.name}
513
- entry={path.relative(process.cwd(), filename)}
514
- buildCommand={config.build || {}}
515
- format={format}
516
- initialMode={args.local ? "local" : "remote"}
517
- jsxFactory={args["jsx-factory"] || envRootObj?.jsx_factory}
518
- jsxFragment={args["jsx-fragment"] || envRootObj?.jsx_fragment}
519
- accountId={config.account_id}
520
- site={args.site || config.site?.bucket}
521
- port={args.port || config.dev?.port}
522
- public={args["experimental-public"]}
523
- compatibilityDate={
524
- args["compatibility-date"] ||
525
- config.compatibility_date ||
526
- new Date().toISOString().substring(0, 10)
527
- }
528
- compatibilityFlags={
529
- (args["compatibility-flags"] as string[]) ||
530
- config.compatibility_flags
531
- }
532
- usageModel={config.usage_model}
533
- bindings={{
534
- kv_namespaces: envRootObj.kv_namespaces?.map(
535
- ({ binding, preview_id, id: _id }) => {
536
- // In `dev`, we make folks use a separate kv namespace called
537
- // `preview_id` instead of `id` so that they don't
538
- // break production data. So here we check that a `preview_id`
539
- // has actually been configured.
540
- // This whole block of code will be obsoleted in the future
541
- // when we have copy-on-write for previews on edge workers.
542
- if (!preview_id) {
543
- // TODO: This error has to be a _lot_ better, ideally just asking
544
- // to create a preview namespace for the user automatically
545
- throw new Error(
546
- `In development, you should use a separate kv namespace than the one you'd use in production. Please create a new kv namespace with "wrangler kv:namespace create <name> --preview" and add its id as preview_id to the kv_namespace "${binding}" in your wrangler.toml`
547
- ); // Ugh, I really don't like this message very much
548
- }
549
- return {
550
- binding,
551
- id: preview_id,
552
- };
553
- }
554
- ),
555
- vars: envRootObj.vars,
556
- durable_objects: envRootObj.durable_objects,
557
- services: envRootObj.experimental_services,
558
- }}
559
- />
560
- );
561
- await waitUntilExit();
562
- }
563
- );
564
-
565
- // publish
566
- wrangler.command(
567
- "publish [script]",
568
- "🆙 Publish your Worker to Cloudflare.",
569
- (yargs) => {
570
- return yargs
571
- .option("env", {
572
- type: "string",
573
- describe: "Perform on a specific environment",
574
- })
575
- .positional("script", {
576
- describe: "script to upload",
577
- type: "string",
578
- })
579
- .option("name", {
580
- describe: "name to use when uploading",
581
- type: "string",
582
- })
583
- .option("compatibility-date", {
584
- describe: "Date to use for compatibility checks",
585
- type: "string",
586
- })
587
- .option("compatibility-flags", {
588
- describe: "Flags to use for compatibility checks",
589
- type: "array",
590
- alias: "compatibility-flag",
591
- })
592
- .option("latest", {
593
- describe: "Use the latest version of the worker runtime",
594
- type: "boolean",
595
- default: false,
596
- })
597
- .option("experimental-public", {
598
- describe: "Static assets to be served",
599
- type: "string",
600
- })
601
- .option("site", {
602
- describe: "Root folder of static assets for Workers Sites",
603
- type: "string",
604
- })
605
- .option("triggers", {
606
- describe: "cron schedules to attach",
607
- alias: ["schedule", "schedules"],
608
- type: "array",
609
- })
610
- .option("routes", {
611
- describe: "routes to upload",
612
- alias: "route",
613
- type: "array",
614
- })
615
- .option("services", {
616
- describe: "experimental support for services",
617
- type: "boolean",
618
- default: "false",
619
- hidden: true,
620
- })
621
- .option("jsx-factory", {
622
- describe: "The function that is called for each JSX element",
623
- type: "string",
624
- })
625
- .option("jsx-fragment", {
626
- describe: "The function that is called for each JSX fragment",
627
- type: "string",
628
- });
629
- },
630
- async (args) => {
631
- if (args.local) {
632
- throw new NotImplementedError(
633
- "🚫 Local publishing is not yet supported"
634
- );
635
- }
636
-
637
- if (args["experimental-public"]) {
638
- console.warn(
639
- "🚨 The --experimental-public field is experimental and will change in the future."
640
- );
641
- }
642
- if (args.public) {
643
- throw new Error(
644
- "🚨 The --public field has been renamed to --experimental-public, and will change behaviour in the future."
645
- );
646
- }
647
-
648
- const config = args.config as Config;
649
-
650
- if (args.latest) {
651
- console.warn(
652
- "⚠️ Using the latest version of the Workers runtime. To silence this warning, please choose a specific version of the runtime with --compatibility-date, or add a compatibility_date to your wrangler.toml.\n"
653
- );
654
- }
655
-
656
- if (!args.local) {
657
- // -- snip, extract --
658
- const loggedIn = await loginOrRefreshIfRequired();
659
- if (!loggedIn) {
660
- // didn't login, let's just quit
661
- console.log("Did not login, quitting...");
662
- return;
663
- }
664
-
665
- if (!config.account_id) {
666
- config.account_id = await getAccountId();
667
- if (!config.account_id) {
668
- throw new Error("No account id found, quitting...");
669
- }
670
- }
671
- // -- snip, end --
672
- }
673
-
674
- await publish({
675
- config: args.config as Config,
676
- name: args.name,
677
- script: args.script,
678
- env: args.env,
679
- compatibilityDate: args.latest
680
- ? new Date().toISOString().substring(0, 10)
681
- : args["compatibility-date"],
682
- compatibilityFlags: args["compatibility-flags"] as string[],
683
- triggers: args.triggers,
684
- jsxFactory: args["jsx-factory"],
685
- jsxFragment: args["jsx-fragment"],
686
- routes: args.routes,
687
- public: args["experimental-public"],
688
- site: args.site,
689
- });
690
- }
691
- );
692
-
693
- // tail
694
- wrangler.command(
695
- "tail [name]",
696
- "🦚 Starts a log tailing session for a deployed Worker.",
697
- (yargs) => {
698
- return (
699
- yargs
700
- .positional("name", {
701
- describe: "name of the worker",
702
- type: "string",
703
- })
704
- // TODO: auto-detect if this should be json or pretty based on atty
705
- .option("format", {
706
- default: "json",
707
- choices: ["json", "pretty"],
708
- describe: "The format of log entries",
709
- })
710
- .option("status", {
711
- choices: ["ok", "error", "canceled"],
712
- describe: "Filter by invocation status",
713
- })
714
- .option("header", {
715
- type: "string",
716
- describe: "Filter by HTTP header",
717
- })
718
- .option("method", {
719
- type: "string",
720
- describe: "Filter by HTTP method",
721
- })
722
- .option("sampling-rate", {
723
- type: "number",
724
- describe: "Adds a percentage of requests to log sampling rate",
725
- })
726
- .option("search", {
727
- type: "string",
728
- describe: "Filter by a text match in console.log messages",
729
- })
730
- .option("env", {
731
- type: "string",
732
- describe: "Perform on a specific environment",
733
- })
734
- );
735
- // TODO: filter by client ip, which can be 'self' or an ip address
736
- },
737
- async (args) => {
738
- if (args.local) {
739
- throw new NotImplementedError(
740
- `local mode is not yet supported for this command`
741
- );
742
- }
743
-
744
- const config = args.config as Config;
745
-
746
- if (!(args.name || config.name)) {
747
- throw new Error("Missing script name");
748
- }
749
- const scriptName = `${args.name || config.name}${
750
- args.env ? `-${args.env}` : ""
751
- }`;
752
-
753
- // -- snip, extract --
754
- const loggedIn = await loginOrRefreshIfRequired();
755
- if (!loggedIn) {
756
- // didn't login, let's just quit
757
- console.log("Did not login, quitting...");
758
- return;
759
- }
760
-
761
- if (!config.account_id) {
762
- config.account_id = await getAccountId();
763
- if (!config.account_id) {
764
- throw new Error("No account id found, quitting...");
765
- }
766
- }
767
- // -- snip, end --
768
-
769
- const accountId = config.account_id;
770
-
771
- const filters = {
772
- status: args.status as "ok" | "error" | "canceled",
773
- header: args.header,
774
- method: args.method,
775
- "sampling-rate": args["sampling-rate"],
776
- search: args.search,
777
- };
778
-
779
- const { tail, expiration, /* sendHeartbeat, */ deleteTail } =
780
- await createTail(accountId, scriptName, filters);
781
-
782
- console.log(
783
- `successfully created tail, expires at ${expiration.toLocaleString()}`
784
- );
785
-
786
- onExit(async () => {
787
- tail.terminate();
788
- await deleteTail();
789
- });
790
-
791
- tail.on("message", (data) => {
792
- console.log(JSON.stringify(JSON.parse(data.toString()), null, " "));
793
- });
794
-
795
- while (tail.readyState !== tail.OPEN) {
796
- switch (tail.readyState) {
797
- case tail.CONNECTING:
798
- await setTimeout(1000);
799
- break;
800
- case tail.CLOSING:
801
- await setTimeout(1000);
802
- break;
803
- case tail.CLOSED:
804
- process.exit(1);
805
- }
806
- }
807
-
808
- console.log(`Connected to ${scriptName}, waiting for logs...`);
809
- }
810
- );
811
-
812
- // preview
813
- wrangler.command(
814
- "preview [method] [body]",
815
- false,
816
- (yargs) => {
817
- return yargs
818
- .positional("method", {
819
- describe: "Type of request to preview your worker",
820
- choices: ["GET", "POST"],
821
- default: ["GET"],
822
- })
823
- .positional("body", {
824
- type: "string",
825
- describe: "Body string to post to your preview worker request.",
826
- default: "Null",
827
- })
828
- .option("env", {
829
- type: "string",
830
- describe: "Perform on a specific environment",
831
- })
832
- .option("watch", {
833
- default: true,
834
- describe: "Enable live preview",
835
- type: "boolean",
836
- });
837
- },
838
- () => {
839
- // "🔬 [DEPRECATED] Preview your code temporarily on https://cloudflareworkers.com"
840
- throw new DeprecationError(
841
- "`wrangler preview` has been deprecated, please refer to TODO://some/path for alternatives"
842
- );
843
- }
844
- );
845
-
846
- // route
847
- wrangler.command(
848
- "route",
849
- false, // I think we want to hide this command
850
- // "➡️ List or delete worker routes",
851
- (routeYargs) => {
852
- return routeYargs
853
- .command(
854
- "list",
855
- "List a route associated with a zone",
856
- (yargs) => {
857
- return yargs
858
- .option("env", {
859
- type: "string",
860
- describe: "Perform on a specific environment",
861
- })
862
- .option("zone", {
863
- type: "string",
864
- describe: "zone id",
865
- })
866
- .positional("zone", {
867
- describe: "zone id",
868
- type: "string",
869
- });
870
- },
871
- async (args) => {
872
- console.log(":route list", args);
873
- // TODO: use environment (current wrangler doesn't do so?)
874
- const zone = args.zone || (args.config as Config).zone_id;
875
- if (!zone) {
876
- throw new Error("missing zone id");
877
- }
878
-
879
- console.log(await fetchResult(`/zones/${zone}/workers/routes`));
880
- }
881
- )
882
- .command(
883
- "delete <id>",
884
- "Delete a route associated with a zone",
885
- (yargs) => {
886
- return yargs
887
- .positional("id", {
888
- describe: "The hash of the route ID to delete.",
889
- type: "string",
890
- })
891
- .option("zone", {
892
- type: "string",
893
- describe: "zone id",
894
- })
895
- .option("env", {
896
- type: "string",
897
- describe: "Perform on a specific environment",
898
- });
899
- },
900
- async (args) => {
901
- console.log(":route delete", args);
902
- // TODO: use environment (current wrangler doesn't do so?)
903
- const zone = args.zone || (args.config as Config).zone_id;
904
- if (!zone) {
905
- throw new Error("missing zone id");
906
- }
907
-
908
- console.log(
909
- await fetchResult(`/zones/${zone}/workers/routes/${args.id}`, {
910
- method: "DELETE",
911
- })
912
- );
913
- }
914
- );
915
- }
916
- );
917
-
918
- // subdomain
919
- wrangler.command(
920
- "subdomain [name]",
921
- false,
922
- // "👷 Create or change your workers.dev subdomain.",
923
- (yargs) => {
924
- return yargs.positional("name", { type: "string" });
925
- },
926
- () => {
927
- throw new DeprecationError(
928
- "`wrangler subdomain` has been deprecated, please refer to TODO://some/path for alternatives"
929
- );
930
- }
931
- );
932
-
933
- // secret
934
- wrangler.command(
935
- "secret",
936
- "🤫 Generate a secret that can be referenced in the worker script",
937
- (secretYargs) => {
938
- return secretYargs
939
- .command(
940
- "put <key>",
941
- "Create or update a secret variable for a script",
942
- (yargs) => {
943
- return yargs
944
- .positional("key", {
945
- describe: "The variable name to be accessible in the script.",
946
- type: "string",
947
- })
948
- .option("name", {
949
- describe: "name of the script",
950
- type: "string",
951
- })
952
- .option("env", {
953
- type: "string",
954
- describe:
955
- "Binds the secret to the script of the specific environment.",
956
- });
957
- },
958
- async (args) => {
959
- if (args.local) {
960
- throw new NotImplementedError(
961
- "--local not implemented for this command yet"
962
- );
963
- }
964
- const config = args.config as Config;
965
-
966
- // TODO: use environment (how does current wrangler do it?)
967
- const scriptName = args.name || config.name;
968
- if (!scriptName) {
969
- throw new Error("Missing script name");
970
- }
971
-
972
- if (!args.local) {
973
- // -- snip, extract --
974
- const loggedIn = await loginOrRefreshIfRequired();
975
- if (!loggedIn) {
976
- // didn't login, let's just quit
977
- console.log("Did not login, quitting...");
978
- return;
979
- }
980
-
981
- if (!config.account_id) {
982
- config.account_id = await getAccountId();
983
- if (!config.account_id) {
984
- throw new Error("No account id found, quitting...");
985
- }
986
- }
987
- // -- snip, end --
988
- }
989
-
990
- const secretValue = await prompt(
991
- "Enter a secret value:",
992
- "password"
993
- );
994
- async function submitSecret() {
995
- return await fetchResult(
996
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets/`,
997
- {
998
- method: "PUT",
999
- headers: { "Content-Type": "application/json" },
1000
- body: JSON.stringify({
1001
- name: args.key,
1002
- text: secretValue,
1003
- type: "secret_text",
1004
- }),
1005
- }
1006
- );
1007
- }
1008
-
1009
- try {
1010
- console.log(await submitSecret());
1011
- } catch (e) {
1012
- if (e.code === 10007) {
1013
- // upload a draft worker
1014
- await fetchResult(
1015
- `/accounts/${config.account_id}/workers/scripts/${scriptName}`,
1016
- {
1017
- method: "PUT",
1018
- // @ts-expect-error TODO: fix this error!
1019
- body: toFormData({
1020
- main: {
1021
- name: scriptName,
1022
- content: `export default { fetch() {} }`,
1023
- type: "esm",
1024
- },
1025
- bindings: {
1026
- kv_namespaces: [],
1027
- vars: {},
1028
- durable_objects: { bindings: [] },
1029
- },
1030
- modules: [],
1031
- }),
1032
- }
1033
- );
1034
-
1035
- // and then try again
1036
- console.log(await submitSecret());
1037
- // TODO: delete the draft worker if this failed too?
1038
- }
1039
- }
1040
- }
1041
- )
1042
- .command(
1043
- "delete <key>",
1044
- "Delete a secret variable from a script",
1045
- (yargs) => {
1046
- return yargs
1047
- .positional("key", {
1048
- describe: "The variable name to be accessible in the script.",
1049
- type: "string",
1050
- })
1051
- .option("name", {
1052
- describe: "name of the script",
1053
- type: "string",
1054
- })
1055
- .option("env", {
1056
- type: "string",
1057
- describe:
1058
- "Binds the secret to the script of the specific environment.",
1059
- });
1060
- },
1061
- async (args) => {
1062
- if (args.local) {
1063
- throw new NotImplementedError(
1064
- "--local not implemented for this command yet"
1065
- );
1066
- }
1067
- const config = args.config as Config;
1068
-
1069
- // TODO: use environment (how does current wrangler do it?)
1070
- const scriptName = args.name || config.name;
1071
- if (!scriptName) {
1072
- throw new Error("Missing script name");
1073
- }
1074
-
1075
- if (!args.local) {
1076
- // -- snip, extract --
1077
- const loggedIn = await loginOrRefreshIfRequired();
1078
- if (!loggedIn) {
1079
- // didn't login, let's just quit
1080
- console.log("Did not login, quitting...");
1081
- return;
1082
- }
1083
-
1084
- if (!config.account_id) {
1085
- config.account_id = await getAccountId();
1086
- if (!config.account_id) {
1087
- throw new Error("No account id found, quitting...");
1088
- }
1089
- }
1090
- // -- snip, end --
1091
- }
1092
-
1093
- if (await confirm("Are you sure you want to delete this secret?")) {
1094
- console.log(
1095
- `Deleting the secret ${args.key} on script ${scriptName}.`
1096
- );
1097
-
1098
- console.log(
1099
- await fetchResult(
1100
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets/${args.key}`,
1101
- { method: "DELETE" }
1102
- )
1103
- );
1104
- }
1105
- }
1106
- )
1107
- .command(
1108
- "list",
1109
- "List all secrets for a script",
1110
- (yargs) => {
1111
- return yargs
1112
- .option("name", {
1113
- describe: "name of the script",
1114
- type: "string",
1115
- })
1116
- .option("env", {
1117
- type: "string",
1118
- describe:
1119
- "Binds the secret to the script of the specific environment.",
1120
- });
1121
- },
1122
- async (args) => {
1123
- if (args.local) {
1124
- throw new NotImplementedError(
1125
- "--local not implemented for this command yet"
1126
- );
1127
- }
1128
- const config = args.config as Config;
1129
-
1130
- // TODO: use environment (how does current wrangler do it?)
1131
- const scriptName = args.name || config.name;
1132
- if (!scriptName) {
1133
- throw new Error("Missing script name");
1134
- }
1135
-
1136
- if (!args.local) {
1137
- // -- snip, extract --
1138
- const loggedIn = await loginOrRefreshIfRequired();
1139
- if (!loggedIn) {
1140
- // didn't login, let's just quit
1141
- console.log("Did not login, quitting...");
1142
- return;
1143
- }
1144
-
1145
- if (!config.account_id) {
1146
- config.account_id = await getAccountId();
1147
- if (!config.account_id) {
1148
- throw new Error("No account id found, quitting...");
1149
- }
1150
- }
1151
- // -- snip, end --
1152
- }
1153
-
1154
- console.log(
1155
- await fetchResult(
1156
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets`
1157
- )
1158
- );
1159
- }
1160
- );
1161
- }
1162
- );
1163
-
1164
- // kv
1165
- // :namespace
1166
- wrangler.command(
1167
- "kv:namespace",
1168
- "🗂️ Interact with your Workers KV Namespaces",
1169
- (namespaceYargs) => {
1170
- return namespaceYargs
1171
- .command(
1172
- "create <namespace>",
1173
- "Create a new namespace",
1174
- (yargs) => {
1175
- return yargs
1176
- .positional("namespace", {
1177
- describe: "The name of the new namespace",
1178
- type: "string",
1179
- demandOption: true,
1180
- })
1181
- .option("env", {
1182
- type: "string",
1183
- describe: "Perform on a specific environment",
1184
- })
1185
- .option("preview", {
1186
- type: "boolean",
1187
- describe: "Interact with a preview namespace",
1188
- });
1189
- },
1190
- async (args) => {
1191
- if (args._.length > 2) {
1192
- const extraArgs = args._.slice(2).join(" ");
1193
- throw new CommandLineArgsError(
1194
- `Unexpected additional positional arguments "${extraArgs}".`
1195
- );
1196
- }
1197
-
1198
- if (!isValidNamespaceBinding(args.namespace)) {
1199
- throw new CommandLineArgsError(
1200
- `The namespace binding name "${args.namespace}" is invalid. It can only have alphanumeric and _ characters, and cannot begin with a number.`
1201
- );
1202
- }
1203
-
1204
- const config = args.config as Config;
1205
- if (!config.name) {
1206
- console.warn(
1207
- "No configured name present, using `worker` as a prefix for the title"
1208
- );
1209
- }
1210
-
1211
- const name = config.name || "worker";
1212
- const environment = args.env ? `-${args.env}` : "";
1213
- const preview = args.preview ? "_preview" : "";
1214
- const title = `${name}${environment}-${args.namespace}${preview}`;
1215
-
1216
- if (args.local) {
1217
- const { Miniflare } = await import("miniflare");
1218
- const mf = new Miniflare({
1219
- kvPersist: (args.kvPersist as string) || true,
1220
- // TODO: these options shouldn't be required
1221
- script: ` `, // has to be a string with at least one char
1222
- });
1223
- await mf.getKVNamespace(title); // this should "create" the namespace
1224
- console.log(`✨ Success! Created KV namespace ${title}`);
1225
- } else {
1226
- // -- snip, extract --
1227
- const loggedIn = await loginOrRefreshIfRequired();
1228
- if (!loggedIn) {
1229
- // didn't login, let's just quit
1230
- console.log("Did not login, quitting...");
1231
- return;
1232
- }
1233
-
1234
- if (!config.account_id) {
1235
- config.account_id = await getAccountId();
1236
- if (!config.account_id) {
1237
- throw new Error("No account id found, quitting...");
1238
- }
1239
- }
1240
- // -- snip, end --
1241
-
1242
- // TODO: generate a binding name stripping non alphanumeric chars
1243
-
1244
- console.log(`🌀 Creating namespace with title "${title}"`);
1245
- const namespaceId = await createNamespace(
1246
- config.account_id,
1247
- title
1248
- );
1249
-
1250
- console.log("✨ Success!");
1251
- const envString = args.env ? ` under [env.${args.env}]` : "";
1252
- const previewString = args.preview ? "preview_" : "";
1253
- console.log(
1254
- `Add the following to your configuration file in your kv_namespaces array${envString}:`
1255
- );
1256
- console.log(
1257
- `{ binding = "${args.namespace}", ${previewString}id = "${namespaceId}" }`
1258
- );
1259
-
1260
- // TODO: automatically write this block to the wrangler.toml config file??
1261
- }
1262
- }
1263
- )
1264
- .command(
1265
- "list",
1266
- "Outputs a list of all KV namespaces associated with your account id.",
1267
- {},
1268
- async (args) => {
1269
- const config = args.config as Config;
1270
-
1271
- if (args.local) {
1272
- throw new NotImplementedError(
1273
- `local mode is not yet supported for this command`
1274
- );
1275
- } else {
1276
- // -- snip, extract --
1277
- const loggedIn = await loginOrRefreshIfRequired();
1278
- if (!loggedIn) {
1279
- // didn't login, let's just quit
1280
- console.log("Did not login, quitting...");
1281
- return;
1282
- }
1283
-
1284
- if (!config.account_id) {
1285
- config.account_id = await getAccountId();
1286
- if (!config.account_id) {
1287
- throw new Error("No account id found, quitting...");
1288
- }
1289
- }
1290
- // -- snip, end --
1291
-
1292
- // TODO: we should show bindings if they exist for given ids
1293
-
1294
- console.log(
1295
- JSON.stringify(
1296
- await listNamespaces(config.account_id),
1297
- null,
1298
- " "
1299
- )
1300
- );
1301
- }
1302
- }
1303
- )
1304
- .command(
1305
- "delete",
1306
- "Deletes a given namespace.",
1307
- (yargs) => {
1308
- return yargs
1309
- .option("binding", {
1310
- type: "string",
1311
- describe: "The name of the namespace to delete",
1312
- })
1313
- .option("namespace-id", {
1314
- type: "string",
1315
- describe: "The id of the namespace to delete",
1316
- })
1317
- .check(demandOneOfOption("binding", "namespace-id"))
1318
- .option("env", {
1319
- type: "string",
1320
- describe: "Perform on a specific environment",
1321
- })
1322
- .option("preview", {
1323
- type: "boolean",
1324
- describe: "Interact with a preview namespace",
1325
- });
1326
- },
1327
- async (args) => {
1328
- const config = args.config as Config;
1329
-
1330
- if (args.local) {
1331
- throw new NotImplementedError(
1332
- `local mode is not yet supported for this command`
1333
- );
1334
- } else {
1335
- let id;
1336
- try {
1337
- id = getNamespaceId(args);
1338
- } catch (e) {
1339
- throw new CommandLineArgsError(
1340
- "Not able to delete namespace.\n" + e.message
1341
- );
1342
- }
1343
-
1344
- // -- snip, extract --
1345
- const loggedIn = await loginOrRefreshIfRequired();
1346
- if (!loggedIn) {
1347
- // didn't login, let's just quit
1348
- console.log("Did not login, quitting...");
1349
- return;
1350
- }
1351
-
1352
- if (!config.account_id) {
1353
- config.account_id = await getAccountId();
1354
- if (!config.account_id) {
1355
- throw new Error("No account id found, quitting...");
1356
- }
1357
- }
1358
- // -- snip, end --
1359
-
1360
- await fetchResult<{ id: string }>(
1361
- `/accounts/${config.account_id}/storage/kv/namespaces/${id}`,
1362
- { method: "DELETE" }
1363
- );
1364
-
1365
- // TODO: recommend they remove it from wrangler.toml
1366
-
1367
- // test-mf wrangler kv:namespace delete --namespace-id 2a7d3d8b23fc4159b5afa489d6cfd388
1368
- // Are you sure you want to delete namespace 2a7d3d8b23fc4159b5afa489d6cfd388? [y/n]
1369
- // n
1370
- // 💁 Not deleting namespace 2a7d3d8b23fc4159b5afa489d6cfd388
1371
- // ➜ test-mf wrangler kv:namespace delete --namespace-id 2a7d3d8b23fc4159b5afa489d6cfd388
1372
- // Are you sure you want to delete namespace 2a7d3d8b23fc4159b5afa489d6cfd388? [y/n]
1373
- // y
1374
- // 🌀 Deleting namespace 2a7d3d8b23fc4159b5afa489d6cfd388
1375
- // ✨ Success
1376
- // ⚠️ Make sure to remove this "kv-namespace" entry from your configuration file!
1377
- // ➜ test-mf
1378
-
1379
- // TODO: do it automatically
1380
-
1381
- // TODO: delete the preview namespace as well?
1382
- }
1383
- }
1384
- );
1385
- }
1386
- );
1387
-
1388
- // :key
1389
- wrangler.command(
1390
- "kv:key",
1391
- "🔑 Individually manage Workers KV key-value pairs",
1392
- (kvKeyYargs) => {
1393
- return kvKeyYargs
1394
- .command(
1395
- "put <key> [value]",
1396
- "Writes a single key/value pair to the given namespace.",
1397
- (yargs) => {
1398
- return yargs
1399
- .positional("key", {
1400
- type: "string",
1401
- describe: "The key to write to.",
1402
- demandOption: true,
1403
- })
1404
- .positional("value", {
1405
- type: "string",
1406
- describe: "The value to write.",
1407
- })
1408
- .option("binding", {
1409
- type: "string",
1410
- describe: "The binding of the namespace to write to.",
1411
- })
1412
- .option("namespace-id", {
1413
- type: "string",
1414
- describe: "The id of the namespace to write to.",
1415
- })
1416
- .check(demandOneOfOption("binding", "namespace-id"))
1417
- .option("env", {
1418
- type: "string",
1419
- describe: "Perform on a specific environment",
1420
- })
1421
- .option("preview", {
1422
- type: "boolean",
1423
- describe: "Interact with a preview namespace",
1424
- })
1425
- .option("ttl", {
1426
- type: "number",
1427
- describe: "Time for which the entries should be visible.",
1428
- })
1429
- .option("expiration", {
1430
- type: "number",
1431
- describe:
1432
- "Time since the UNIX epoch after which the entry expires",
1433
- })
1434
- .option("path", {
1435
- type: "string",
1436
- describe: "Read value from the file at a given path.",
1437
- })
1438
- .check(demandOneOfOption("value", "path"));
1439
- },
1440
- async ({ key, ttl, expiration, ...args }) => {
1441
- const namespaceId = getNamespaceId(args);
1442
- // One of `args.path` and `args.value` must be defined
1443
- const value = args.path
1444
- ? await readFile(args.path, "utf-8")
1445
- : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1446
- args.value!;
1447
- const config = args.config as Config;
1448
-
1449
- if (args.path) {
1450
- console.log(
1451
- `writing the contents of ${args.path} to the key "${key}" on namespace ${namespaceId}`
1452
- );
1453
- } else {
1454
- console.log(
1455
- `writing the value "${value}" to key "${key}" on namespace ${namespaceId}`
1456
- );
1457
- }
1458
-
1459
- if (args.local) {
1460
- const { Miniflare } = await import("miniflare");
1461
- const mf = new Miniflare({
1462
- kvPersist: (args.kvPersist as string) || true,
1463
- // TODO: these options shouldn't be required
1464
- script: ` `, // has to be a string with at least one char
1465
- });
1466
- const ns = await mf.getKVNamespace(namespaceId);
1467
- await ns.put(key, value, { expiration, expirationTtl: ttl });
1468
- } else {
1469
- // -- snip, extract --
1470
- const loggedIn = await loginOrRefreshIfRequired();
1471
- if (!loggedIn) {
1472
- // didn't login, let's just quit
1473
- console.log("Did not login, quitting...");
1474
- return;
1475
- }
1476
-
1477
- if (!config.account_id) {
1478
- config.account_id = await getAccountId();
1479
- if (!config.account_id) {
1480
- throw new Error("No account id found, quitting...");
1481
- }
1482
- }
1483
- // -- snip, end --
1484
-
1485
- await putKeyValue(config.account_id, namespaceId, key, value, {
1486
- expiration,
1487
- expiration_ttl: ttl,
1488
- });
1489
- }
1490
- }
1491
- )
1492
- .command(
1493
- "list",
1494
- "Outputs a list of all keys in a given namespace.",
1495
- (yargs) => {
1496
- return yargs
1497
- .option("binding", {
1498
- type: "string",
1499
- describe: "The name of the namespace to list",
1500
- })
1501
- .option("namespace-id", {
1502
- type: "string",
1503
- describe: "The id of the namespace to list",
1504
- })
1505
- .check(demandOneOfOption("binding", "namespace-id"))
1506
- .option("env", {
1507
- type: "string",
1508
- describe: "Perform on a specific environment",
1509
- })
1510
- .option("preview", {
1511
- type: "boolean",
1512
- // In the case of listing keys we will default to non-preview mode
1513
- default: false,
1514
- describe: "Interact with a preview namespace",
1515
- })
1516
- .option("prefix", {
1517
- type: "string",
1518
- describe: "A prefix to filter listed keys",
1519
- });
1520
- },
1521
- async ({ prefix, ...args }) => {
1522
- // TODO: support for limit+cursor (pagination)
1523
-
1524
- const namespaceId = getNamespaceId(args);
1525
- const config = args.config as Config;
1526
-
1527
- if (args.local) {
1528
- const { Miniflare } = await import("miniflare");
1529
- const mf = new Miniflare({
1530
- kvPersist: (args.kvPersist as string) || true,
1531
- // TODO: these options shouldn't be required
1532
- script: ` `, // has to be a string with at least one char
1533
- });
1534
- const ns = await mf.getKVNamespace(namespaceId);
1535
- const listResponse = await ns.list({ prefix });
1536
- console.log(JSON.stringify(listResponse.keys, null, " ")); // TODO: paginate, collate
1537
- } else {
1538
- // -- snip, extract --
1539
- const loggedIn = await loginOrRefreshIfRequired();
1540
- if (!loggedIn) {
1541
- // didn't login, let's just quit
1542
- console.log("Did not login, quitting...");
1543
- return;
1544
- }
1545
-
1546
- if (!config.account_id) {
1547
- config.account_id = await getAccountId();
1548
- if (!config.account_id) {
1549
- throw new Error("No account id found, quitting...");
1550
- }
1551
- }
1552
- // -- snip, end --
1553
-
1554
- console.log(
1555
- await listNamespaceKeys(config.account_id, namespaceId, prefix)
1556
- );
1557
- }
1558
- }
1559
- )
1560
- .command(
1561
- "get <key>",
1562
- "Reads a single value by key from the given namespace.",
1563
- (yargs) => {
1564
- return yargs
1565
- .positional("key", {
1566
- describe: "The key value to get.",
1567
- type: "string",
1568
- demandOption: true,
1569
- })
1570
- .option("binding", {
1571
- type: "string",
1572
- describe: "The name of the namespace to get from",
1573
- })
1574
- .option("namespace-id", {
1575
- type: "string",
1576
- describe: "The id of the namespace to get from",
1577
- })
1578
- .check(demandOneOfOption("binding", "namespace-id"))
1579
- .option("env", {
1580
- type: "string",
1581
- describe: "Perform on a specific environment",
1582
- })
1583
- .option("preview", {
1584
- type: "boolean",
1585
- describe: "Interact with a preview namespace",
1586
- })
1587
- .option("preview", {
1588
- type: "boolean",
1589
- // In the case of getting key values we will default to non-preview mode
1590
- default: false,
1591
- describe: "Interact with a preview namespace",
1592
- });
1593
- },
1594
- async ({ key, ...args }) => {
1595
- const namespaceId = getNamespaceId(args);
1596
- const config = args.config as Config;
1597
-
1598
- if (args.local) {
1599
- const { Miniflare } = await import("miniflare");
1600
- const mf = new Miniflare({
1601
- kvPersist: (args.kvPersist as string) || true,
1602
- // TODO: these options shouldn't be required
1603
- script: ` `, // has to be a string with at least one char
1604
- });
1605
- const ns = await mf.getKVNamespace(namespaceId);
1606
- console.log(await ns.get(key));
1607
- return;
1608
- }
1609
-
1610
- if (!args.local) {
1611
- // -- snip, extract --
1612
- const loggedIn = await loginOrRefreshIfRequired();
1613
- if (!loggedIn) {
1614
- // didn't login, let's just quit
1615
- console.log("Did not login, quitting...");
1616
- return;
1617
- }
1618
-
1619
- if (!config.account_id) {
1620
- config.account_id = await getAccountId();
1621
- if (!config.account_id) {
1622
- throw new Error("No account id found, quitting...");
1623
- }
1624
- }
1625
- // -- snip, end --
1626
- }
1627
-
1628
- console.log(
1629
- await fetchRaw(
1630
- `/accounts/${config.account_id}/storage/kv/namespaces/${namespaceId}/values/${key}`
1631
- )
1632
- );
1633
- }
1634
- )
1635
- .command(
1636
- "delete <key>",
1637
- "Removes a single key value pair from the given namespace.",
1638
- (yargs) => {
1639
- return yargs
1640
- .positional("key", {
1641
- describe: "The key value to delete",
1642
- type: "string",
1643
- demandOption: true,
1644
- })
1645
- .option("binding", {
1646
- type: "string",
1647
- describe: "The name of the namespace to delete from",
1648
- })
1649
- .option("namespace-id", {
1650
- type: "string",
1651
- describe: "The id of the namespace to delete from",
1652
- })
1653
- .check(demandOneOfOption("binding", "namespace-id"))
1654
- .option("env", {
1655
- type: "string",
1656
- describe: "Perform on a specific environment",
1657
- })
1658
- .option("preview", {
1659
- type: "boolean",
1660
- describe: "Interact with a preview namespace",
1661
- });
1662
- },
1663
- async ({ key, ...args }) => {
1664
- const namespaceId = getNamespaceId(args);
1665
-
1666
- console.log(
1667
- `deleting the key "${key}" on namespace ${namespaceId}`
1668
- );
1669
-
1670
- if (args.local) {
1671
- const { Miniflare } = await import("miniflare");
1672
- const mf = new Miniflare({
1673
- kvPersist: (args.kvPersist as string) || true,
1674
- // TODO: these options shouldn't be required
1675
- script: ` `, // has to be a string with at least one char
1676
- });
1677
- const ns = await mf.getKVNamespace(namespaceId);
1678
- console.log(await ns.delete(key));
1679
- return;
1680
- }
1681
-
1682
- const config = args.config as Config;
1683
-
1684
- if (!args.local) {
1685
- // -- snip, extract --
1686
- const loggedIn = await loginOrRefreshIfRequired();
1687
- if (!loggedIn) {
1688
- // didn't login, let's just quit
1689
- console.log("Did not login, quitting...");
1690
- return;
1691
- }
1692
-
1693
- if (!config.account_id) {
1694
- config.account_id = await getAccountId();
1695
- if (!config.account_id) {
1696
- throw new Error("No account id found, quitting...");
1697
- }
1698
- }
1699
- // -- snip, end --
1700
- }
1701
-
1702
- await fetchResult(
1703
- `/accounts/${config.account_id}/storage/kv/namespaces/${namespaceId}/values/${key}`,
1704
- { method: "DELETE" }
1705
- );
1706
- }
1707
- );
1708
- }
1709
- );
1710
-
1711
- // :bulk
1712
- wrangler.command(
1713
- "kv:bulk",
1714
- "💪 Interact with multiple Workers KV key-value pairs at once",
1715
- (kvBulkYargs) => {
1716
- return kvBulkYargs
1717
- .command(
1718
- "put <filename>",
1719
- "Upload multiple key-value pairs to a namespace",
1720
- (yargs) => {
1721
- return yargs
1722
- .positional("filename", {
1723
- describe: `The JSON file of key-value pairs to upload, in form [{"key":..., "value":...}"...]`,
1724
- type: "string",
1725
- demandOption: true,
1726
- })
1727
- .option("binding", {
1728
- type: "string",
1729
- describe: "The name of the namespace to put to",
1730
- })
1731
- .option("namespace-id", {
1732
- type: "string",
1733
- describe: "The id of the namespace to put to",
1734
- })
1735
- .check(demandOneOfOption("binding", "namespace-id"))
1736
- .option("env", {
1737
- type: "string",
1738
- describe: "Perform on a specific environment",
1739
- })
1740
- .option("preview", {
1741
- type: "boolean",
1742
- describe: "Interact with a preview namespace",
1743
- });
1744
- },
1745
- async ({ filename, ...args }) => {
1746
- // The simplest implementation I could think of.
1747
- // This could be made more efficient with a streaming parser/uploader
1748
- // but we'll do that in the future if needed.
1749
-
1750
- const namespaceId = getNamespaceId(args);
1751
- const config = args.config as Config;
1752
- const content = await readFile(filename, "utf-8");
1753
- let parsedContent;
1754
- try {
1755
- parsedContent = JSON.parse(content);
1756
- } catch (err) {
1757
- throw new Error(
1758
- `Could not parse json from ${filename}.\n${err.message ?? err}`
1759
- );
1760
- }
1761
-
1762
- if (args.local) {
1763
- const { Miniflare } = await import("miniflare");
1764
- const mf = new Miniflare({
1765
- kvPersist: (args.kvPersist as string) || true,
1766
- // TODO: these options shouldn't be required
1767
- script: ` `, // has to be a string with at least one char
1768
- });
1769
- const ns = await mf.getKVNamespace(namespaceId);
1770
- for (const {
1771
- key,
1772
- value,
1773
- expiration,
1774
- expiration_ttl,
1775
- } of parsedContent) {
1776
- await ns.put(key, value, {
1777
- expiration,
1778
- expirationTtl: expiration_ttl,
1779
- });
1780
- }
1781
- } else {
1782
- // -- snip, extract --
1783
- const loggedIn = await loginOrRefreshIfRequired();
1784
- if (!loggedIn) {
1785
- // didn't login, let's just quit
1786
- console.log("Did not login, quitting...");
1787
- return;
1788
- }
1789
-
1790
- if (!config.account_id) {
1791
- config.account_id = await getAccountId();
1792
- if (!config.account_id) {
1793
- throw new Error("No account id found, quitting...");
1794
- }
1795
- }
1796
- // -- snip, end --
1797
-
1798
- console.log(
1799
- await putBulkKeyValue(config.account_id, namespaceId, content)
1800
- );
1801
- }
1802
- }
1803
- )
1804
- .command(
1805
- "delete <filename>",
1806
- "Delete multiple key-value pairs from a namespace",
1807
- (yargs) => {
1808
- return yargs
1809
- .positional("filename", {
1810
- describe: `The JSON file of keys to delete, in the form ["key1", "key2", ...]`,
1811
- type: "string",
1812
- demandOption: true,
1813
- })
1814
- .option("binding", {
1815
- type: "string",
1816
- describe: "The name of the namespace to delete from",
1817
- })
1818
- .option("namespace-id", {
1819
- type: "string",
1820
- describe: "The id of the namespace to delete from",
1821
- })
1822
- .check(demandOneOfOption("binding", "namespace-id"))
1823
- .option("env", {
1824
- type: "string",
1825
- describe: "Perform on a specific environment",
1826
- })
1827
- .option("preview", {
1828
- type: "boolean",
1829
- describe: "Interact with a preview namespace",
1830
- });
1831
- },
1832
- async ({ filename, ...args }) => {
1833
- const namespaceId = getNamespaceId(args);
1834
- const config = args.config as Config;
1835
- const content = await readFile(filename, "utf-8");
1836
- let parsedContent;
1837
- try {
1838
- parsedContent = JSON.parse(content);
1839
- } catch (err) {
1840
- throw new Error(
1841
- `Could not parse json from ${filename}.\n${err.message ?? err}`
1842
- );
1843
- }
1844
-
1845
- if (args.local) {
1846
- const { Miniflare } = await import("miniflare");
1847
- const mf = new Miniflare({
1848
- kvPersist: (args.kvPersist as string) || true,
1849
- // TODO: these options shouldn't be required
1850
- script: ` `, // has to be a string with at least one char
1851
- });
1852
- const ns = await mf.getKVNamespace(namespaceId);
1853
- for (const key of parsedContent) {
1854
- await ns.delete(key);
1855
- }
1856
- } else {
1857
- // -- snip, extract --
1858
- const loggedIn = await loginOrRefreshIfRequired();
1859
- if (!loggedIn) {
1860
- // didn't login, let's just quit
1861
- console.log("Did not login, quitting...");
1862
- return;
1863
- }
1864
-
1865
- if (!config.account_id) {
1866
- config.account_id = await getAccountId();
1867
- if (!config.account_id) {
1868
- throw new Error("No account id found, quitting...");
1869
- }
1870
- }
1871
- // -- snip, end --
1872
-
1873
- console.log(
1874
- await deleteBulkKeyValue(
1875
- config.account_id,
1876
- namespaceId,
1877
- content
1878
- )
1879
- );
1880
- }
1881
- }
1882
- );
1883
- }
1884
- );
1885
-
1886
- wrangler.command("pages", "⚡️ Configure Cloudflare Pages", pages);
1887
-
1888
- wrangler
1889
- .option("config", {
1890
- alias: "c",
1891
- describe: "Path to .toml configuration file",
1892
- type: "string",
1893
- async coerce(arg) {
1894
- return await readConfig(arg);
1895
- },
1896
- })
1897
- .option("local", {
1898
- alias: "l",
1899
- describe: "Run on my machine",
1900
- type: "boolean",
1901
- default: false, // I bet this will a point of contention. We'll revisit it.
1902
- });
1903
-
1904
- wrangler.group(["config", "help", "version"], "Flags:");
1905
- wrangler.help().alias("h", "help");
1906
- wrangler.version(wranglerVersion).alias("v", "version");
1907
- wrangler.exitProcess(false);
1908
-
1909
- await initialiseUserConfig();
1910
-
1911
- try {
1912
- await wrangler.parse();
1913
- } catch (e) {
1914
- if (e instanceof CommandLineArgsError) {
1915
- wrangler.showHelp("error");
1916
- console.error(""); // Just adds a bit of space
1917
- console.error(e.message);
1918
- } else {
1919
- console.error(e.message);
1920
- console.error(""); // Just adds a bit of space
1921
- console.error(
1922
- `${fgGreenColor}%s${resetColor}`,
1923
- "If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
1924
- );
1925
- }
1926
- throw e;
1927
- }
1849
+ const wrangler = createCLIParser(argv);
1850
+ try {
1851
+ await wrangler.parse();
1852
+ } catch (e) {
1853
+ logger.log(""); // Just adds a bit of space
1854
+ if (e instanceof CommandLineArgsError) {
1855
+ logger.error(e.message);
1856
+ // We are not able to ask the `wrangler` CLI parser to show help for a subcommand programmatically.
1857
+ // The workaround is to re-run the parsing with an additional `--help` flag, which will result in the correct help message being displayed.
1858
+ // The `wrangler` object is "frozen"; we cannot reuse that with different args, so we must create a new CLI parser to generate the help message.
1859
+ await createCLIParser([...argv, "--help"]).parse();
1860
+ } else if (e instanceof ParseError) {
1861
+ e.notes.push({
1862
+ text: "\nIf you think this is a bug, please open an issue at: https://github.com/cloudflare/wrangler2/issues/new/choose",
1863
+ });
1864
+ logger.log(formatMessage(e));
1865
+ } else {
1866
+ logger.error(e instanceof Error ? e.message : e);
1867
+ logger.log(
1868
+ `${fgGreenColor}%s${resetColor}`,
1869
+ "If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
1870
+ );
1871
+ }
1872
+ throw e;
1873
+ }
1874
+ }
1875
+
1876
+ export function getDevCompatibilityDate(
1877
+ config: Config,
1878
+ compatibilityDate = config.compatibility_date
1879
+ ) {
1880
+ const currentDate = new Date().toISOString().substring(0, 10);
1881
+ if (config.configPath !== undefined && compatibilityDate === undefined) {
1882
+ logger.warn(
1883
+ `No compatibility_date was specified. Using today's date: ${currentDate}.\n` +
1884
+ "Add one to your wrangler.toml file:\n" +
1885
+ "```\n" +
1886
+ `compatibility_date = "${currentDate}"\n` +
1887
+ "```\n" +
1888
+ "or pass it in your terminal:\n" +
1889
+ "```\n" +
1890
+ `--compatibility-date=${currentDate}\n` +
1891
+ "```\n" +
1892
+ "See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information."
1893
+ );
1894
+ }
1895
+ return compatibilityDate ?? currentDate;
1928
1896
  }