dsh-edge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +183 -0
  4. package/README.zh.md +183 -0
  5. package/THIRD_PARTY_NOTICES.md +199 -0
  6. package/dist/_headers +3 -0
  7. package/dist/assets/fonts/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  8. package/dist/assets/fonts/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  9. package/dist/assets/fonts/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  10. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  11. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  12. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  13. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  14. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  15. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  16. package/dist/assets/fonts/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  17. package/dist/assets/fonts/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  18. package/dist/assets/fonts/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  19. package/dist/assets/fonts/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  20. package/dist/assets/fonts/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  21. package/dist/assets/fonts/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  22. package/dist/assets/fonts/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  23. package/dist/assets/fonts/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  24. package/dist/assets/fonts/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  25. package/dist/assets/fonts/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  26. package/dist/assets/fonts/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  27. package/dist/assets/fonts/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  28. package/dist/assets/fonts/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  29. package/dist/assets/fonts/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  30. package/dist/assets/fonts/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  31. package/dist/assets/fonts/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  32. package/dist/assets/fonts/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  33. package/dist/assets/fonts/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  34. package/dist/assets/fonts/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  35. package/dist/assets/fonts/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  36. package/dist/assets/fonts/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  37. package/dist/assets/fonts/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  38. package/dist/assets/fonts/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  39. package/dist/assets/fonts/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  40. package/dist/assets/fonts/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  41. package/dist/assets/fonts/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  42. package/dist/assets/fonts/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  43. package/dist/assets/fonts/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  44. package/dist/assets/fonts/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  45. package/dist/assets/fonts/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  46. package/dist/assets/fonts/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  47. package/dist/assets/fonts/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  48. package/dist/assets/fonts/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  49. package/dist/assets/fonts/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  50. package/dist/assets/fonts/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  51. package/dist/assets/fonts/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  52. package/dist/assets/fonts/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  53. package/dist/assets/fonts/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  54. package/dist/assets/fonts/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  55. package/dist/assets/fonts/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  56. package/dist/assets/fonts/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  57. package/dist/assets/fonts/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  58. package/dist/assets/fonts/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  59. package/dist/assets/fonts/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  60. package/dist/assets/fonts/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  61. package/dist/assets/fonts/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  62. package/dist/assets/fonts/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  63. package/dist/assets/fonts/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  64. package/dist/assets/fonts/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  65. package/dist/assets/fonts/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  66. package/dist/assets/index-C-1AiF3k.js +112 -0
  67. package/dist/assets/index-CSGf6Qzd.css +1 -0
  68. package/dist/assets/langs/c-BIGW1oBm.js +2 -0
  69. package/dist/assets/langs/cpp-DIPi6g--.js +2 -0
  70. package/dist/assets/langs/csharp-DSvCPggb.js +2 -0
  71. package/dist/assets/langs/css-CLj8gQPS.js +2 -0
  72. package/dist/assets/langs/go-C27-OAKa.js +2 -0
  73. package/dist/assets/langs/html-CfGypltT.js +2 -0
  74. package/dist/assets/langs/ini-BEwlwnbL.js +2 -0
  75. package/dist/assets/langs/java-CylS5w8V.js +2 -0
  76. package/dist/assets/langs/kotlin-BdnUsdx6.js +2 -0
  77. package/dist/assets/langs/less-B1dDrJ26.js +2 -0
  78. package/dist/assets/langs/lua-BaeVxFsk.js +2 -0
  79. package/dist/assets/langs/markdown-Cvjx9yec.js +2 -0
  80. package/dist/assets/langs/mdx-Cmh6b_Ma.js +2 -0
  81. package/dist/assets/langs/php-D-uCvoST.js +2 -0
  82. package/dist/assets/langs/python-B6aJPvgy.js +2 -0
  83. package/dist/assets/langs/ruby-DEItuUFs.js +2 -0
  84. package/dist/assets/langs/rust-B1yitclQ.js +2 -0
  85. package/dist/assets/langs/scss-D5BDwBP9.js +2 -0
  86. package/dist/assets/langs/sql-CRqJ_cUM.js +2 -0
  87. package/dist/assets/langs/swift-D82vCrfD.js +2 -0
  88. package/dist/assets/langs/toml-vGWfd6FD.js +2 -0
  89. package/dist/assets/langs/xml-sdJ4AIDG.js +2 -0
  90. package/dist/assets/langs/yaml-Buea-lGh.js +2 -0
  91. package/dist/assets/vendor-Cjbwl5VI.js +411 -0
  92. package/dist/assets/vendor-CjyC-hUb.css +1 -0
  93. package/dist/favicon.svg +8 -0
  94. package/dist/index.html +35 -0
  95. package/dist/manifest.webmanifest +16 -0
  96. package/dist/plugins/@deepseek-ai/dsh-api-gateway/client.js +420 -0
  97. package/dist/plugins/@deepseek-ai/dsh-api-remotes/client.js +5937 -0
  98. package/dist/plugins/@deepseek-ai/dsh-client-connection/client.js +10207 -0
  99. package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +1231 -0
  100. package/dist/plugins/@deepseek-ai/dsh-client-modules/client.js +259 -0
  101. package/dist/plugins/@deepseek-ai/dsh-client-runtime/client.js +10539 -0
  102. package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +1725 -0
  103. package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +1116 -0
  104. package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +9832 -0
  105. package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +373 -0
  106. package/dist/plugins/@deepseek-ai/dsh-client-ui-edge/client.js +549 -0
  107. package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +859 -0
  108. package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +280 -0
  109. package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +456 -0
  110. package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +804 -0
  111. package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +461 -0
  112. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings/client.js +254 -0
  113. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +609 -0
  114. package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +292 -0
  115. package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +327 -0
  116. package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +710 -0
  117. package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +1312 -0
  118. package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +1630 -0
  119. package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +7370 -0
  120. package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +700 -0
  121. package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +474 -0
  122. package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +2426 -0
  123. package/dist/plugins/@deepseek-ai/dsh-typert-registry/client.js +1372 -0
  124. package/package.json +97 -0
  125. package/scripts/assemble-web.mjs +230 -0
  126. package/scripts/bundle-size.d.mts +2 -0
  127. package/scripts/bundle-size.mjs +26 -0
  128. package/scripts/bundle.mjs +53 -0
  129. package/scripts/cli.d.mts +39 -0
  130. package/scripts/cli.mjs +496 -0
  131. package/scripts/install.d.mts +149 -0
  132. package/scripts/install.mjs +989 -0
  133. package/scripts/legal-files.mjs +24 -0
  134. package/scripts/verify-packed.mjs +32 -0
  135. package/scripts/wrangler-config.d.mts +18 -0
  136. package/scripts/wrangler-config.mjs +84 -0
  137. package/src/agent.ts +119 -0
  138. package/src/auth.ts +352 -0
  139. package/src/deepseek.ts +231 -0
  140. package/src/deployment.ts +147 -0
  141. package/src/direct-shell-core-empty.ts +10 -0
  142. package/src/direct-shell-protocol.ts +5 -0
  143. package/src/direct-shell.ts +341 -0
  144. package/src/do-session-persistence.ts +1064 -0
  145. package/src/edge-api.ts +796 -0
  146. package/src/edge-credentials.ts +56 -0
  147. package/src/edge-remotes.ts +41 -0
  148. package/src/edge-workspace-store.ts +339 -0
  149. package/src/http.ts +212 -0
  150. package/src/index.ts +196 -0
  151. package/src/instance.ts +1007 -0
  152. package/src/isolated-direct-shell-unavailable.ts +11 -0
  153. package/src/protocol.ts +39 -0
  154. package/src/release.ts +9 -0
  155. package/src/session-store.ts +1110 -0
  156. package/src/sse.ts +86 -0
  157. package/src/web-search.ts +38 -0
  158. package/src/workspace.ts +270 -0
  159. package/wrangler.jsonc +56 -0
@@ -0,0 +1,989 @@
1
+ import { randomBytes, randomUUID } from 'node:crypto'
2
+ import { execFile } from 'node:child_process'
3
+ import { chmod, mkdtemp, open, readFile, rm, writeFile } from 'node:fs/promises'
4
+ import { createRequire } from 'node:module'
5
+ import { tmpdir } from 'node:os'
6
+ import { join } from 'node:path'
7
+ import { setTimeout as sleep } from 'node:timers/promises'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { execa } from 'execa'
10
+ import { writeModeWranglerConfig } from './wrangler-config.mjs'
11
+
12
+ export const DEPLOYMENT_ID_DEFINE = '__DSH_EDGE_DEPLOYMENT_ID__'
13
+ export const DEFAULT_WORKER_NAME = 'dsh-edge'
14
+ export const LOGIN_PROFILE = 'dsh-edge-install'
15
+ export const RUNTIME_MODES = Object.freeze({
16
+ direct: Object.freeze({
17
+ environment: '',
18
+ expectedShell: 'just-bash-direct',
19
+ label: 'Free — Direct Shell',
20
+ }),
21
+ isolated: Object.freeze({
22
+ environment: 'isolated',
23
+ expectedShell: 'just-bash-isolated',
24
+ label: 'Isolated — Dynamic Worker',
25
+ }),
26
+ })
27
+
28
+ const appDirectory = fileURLToPath(new URL('..', import.meta.url))
29
+ const require = createRequire(import.meta.url)
30
+ const WRANGLER_CLI = require.resolve('wrangler')
31
+ const AUTH_ENV_KEYS = Object.freeze([
32
+ 'CLOUDFLARE_ACCOUNT_ID',
33
+ 'CLOUDFLARE_API_KEY',
34
+ 'CLOUDFLARE_API_TOKEN',
35
+ 'CLOUDFLARE_EMAIL',
36
+ 'CF_ACCOUNT_ID',
37
+ 'CF_API_KEY',
38
+ 'CF_API_TOKEN',
39
+ 'CF_EMAIL',
40
+ ])
41
+ const CLOUDFLARE_CONTEXT_ENV_KEYS = Object.freeze([
42
+ 'CLOUDFLARE_API_BASE_URL',
43
+ 'CLOUDFLARE_CONFIG_PATH',
44
+ 'WRANGLER_HOME',
45
+ ])
46
+ const RUNTIME_ENV_KEYS = Object.freeze([
47
+ 'ALL_PROXY',
48
+ 'APPDATA',
49
+ 'CI',
50
+ 'COLORTERM',
51
+ 'COMSPEC',
52
+ 'ComSpec',
53
+ 'DBUS_SESSION_BUS_ADDRESS',
54
+ 'DISPLAY',
55
+ 'FORCE_COLOR',
56
+ 'HOME',
57
+ 'HOMEDRIVE',
58
+ 'HOMEPATH',
59
+ 'HTTPS_PROXY',
60
+ 'HTTP_PROXY',
61
+ 'LANG',
62
+ 'LANGUAGE',
63
+ 'LOCALAPPDATA',
64
+ 'NO_COLOR',
65
+ 'NO_PROXY',
66
+ 'NODE_EXTRA_CA_CERTS',
67
+ 'PATH',
68
+ 'Path',
69
+ 'PATHEXT',
70
+ 'SHELL',
71
+ 'SSL_CERT_DIR',
72
+ 'SSL_CERT_FILE',
73
+ 'SystemRoot',
74
+ 'TEMP',
75
+ 'TERM',
76
+ 'TERM_PROGRAM',
77
+ 'TERM_PROGRAM_VERSION',
78
+ 'TMP',
79
+ 'TMPDIR',
80
+ 'USERPROFILE',
81
+ 'WAYLAND_DISPLAY',
82
+ 'WINDIR',
83
+ 'XDG_CACHE_HOME',
84
+ 'XDG_CONFIG_HOME',
85
+ 'XDG_RUNTIME_DIR',
86
+ 'all_proxy',
87
+ 'https_proxy',
88
+ 'http_proxy',
89
+ 'no_proxy',
90
+ ])
91
+ const CONTROL_CHARACTER = /[\u0000-\u001f\u007f-\u009f]/u
92
+ const BIDI_CONTROL = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u
93
+ const SENSITIVE_ENV_KEY = /(KEY|PASSWORD|SECRET|TOKEN)/iu
94
+ const WORKER_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u
95
+ const CLAIM_URL = /https:\/\/dash\.cloudflare\.com\/claim-preview\?[^\s\u001b]+/u
96
+ const MAX_CAPTURE_BYTES = 2 * 1024 * 1024
97
+
98
+ export class InstallCancelledError extends Error {
99
+ constructor() {
100
+ super('Installation cancelled.')
101
+ this.name = 'InstallCancelledError'
102
+ }
103
+ }
104
+
105
+ export class InstallerOutputError extends Error {
106
+ constructor(stream, cause) {
107
+ super(`Could not write installer ${stream}: ${describeError(cause)}`, { cause })
108
+ this.name = 'InstallerOutputError'
109
+ this.stream = stream
110
+ }
111
+ }
112
+
113
+ /** Return account choices permitted by the selected runtime. */
114
+ export function accountChoices(mode, accounts, command = 'install') {
115
+ requireRuntimeMode(mode)
116
+ const choices = []
117
+ if (mode === 'direct' && command === 'install') {
118
+ choices.push({
119
+ value: 'temporary',
120
+ label: 'Temporary account — no Cloudflare login',
121
+ hint: 'available for 60 minutes; claim it to keep the instance',
122
+ })
123
+ }
124
+ for (const account of accounts) {
125
+ choices.push({
126
+ value: `account:${account.id}`,
127
+ label: account.name,
128
+ hint: `Cloudflare account ${account.id}`,
129
+ })
130
+ }
131
+ choices.push({
132
+ value: 'login',
133
+ label: 'Sign in or create a Cloudflare account',
134
+ hint: 'opens Cloudflare in your browser',
135
+ })
136
+ return choices
137
+ }
138
+
139
+ /** Parse the stable subset of `wrangler whoami --json`. */
140
+ export function parseWhoami(source) {
141
+ let value
142
+ try {
143
+ value = JSON.parse(source)
144
+ } catch {
145
+ throw new Error('Wrangler returned invalid account information.')
146
+ }
147
+ if (value === null || typeof value !== 'object' || value.loggedIn !== true
148
+ || !Array.isArray(value.accounts)) {
149
+ throw new Error('Wrangler did not return an authenticated Cloudflare account.')
150
+ }
151
+ const accounts = value.accounts.map((account) => {
152
+ if (account === null || typeof account !== 'object'
153
+ || typeof account.id !== 'string' || account.id.trim() === ''
154
+ || !isTerminalSafe(account.id)
155
+ || typeof account.name !== 'string' || account.name.trim() === ''
156
+ || !isTerminalSafe(account.name)) {
157
+ throw new Error('Wrangler returned malformed Cloudflare account information.')
158
+ }
159
+ return { id: account.id, name: account.name }
160
+ })
161
+ if (typeof value.email === 'string' && !isTerminalSafe(value.email)) {
162
+ throw new Error('Wrangler returned malformed Cloudflare account information.')
163
+ }
164
+ return { accounts, email: typeof value.email === 'string' ? value.email : undefined }
165
+ }
166
+
167
+ /** Validate the exact Workers service-name format accepted by Wrangler. */
168
+ export function validateWorkerName(value) {
169
+ if (!WORKER_NAME.test(value)) {
170
+ return 'Use 1–63 lowercase letters, numbers, or dashes; start and end with a letter or number.'
171
+ }
172
+ }
173
+
174
+ /** Validate the login secret against the runtime's byte-level contract. */
175
+ export function validateOwnerSecret(value) {
176
+ if (value !== value.trim()) return 'The access key cannot start or end with whitespace.'
177
+ if (CONTROL_CHARACTER.test(value)) return 'The access key cannot contain control characters.'
178
+ if (BIDI_CONTROL.test(value)) return 'The access key cannot contain bidirectional controls.'
179
+ const length = Buffer.byteLength(value, 'utf8')
180
+ if (length < 32 || length > 512) return 'The access key must be 32–512 UTF-8 bytes.'
181
+ }
182
+
183
+ /** Validate a provider key without encoding assumptions that DeepSeek does not promise. */
184
+ export function validateDeepSeekKey(value) {
185
+ if (value === '') return 'Enter a DeepSeek API key.'
186
+ if (value !== value.trim()) return 'The DeepSeek API key cannot start or end with whitespace.'
187
+ if (CONTROL_CHARACTER.test(value)) return 'The DeepSeek API key cannot contain control characters.'
188
+ }
189
+
190
+ export function generateOwnerSecret() {
191
+ return randomBytes(32).toString('base64url')
192
+ }
193
+
194
+ /** Build the least-privilege environment used by authenticated Wrangler commands. */
195
+ export function wranglerEnvironment(environment = process.env) {
196
+ return pickEnvironment(environment, [
197
+ ...RUNTIME_ENV_KEYS,
198
+ ...CLOUDFLARE_CONTEXT_ENV_KEYS,
199
+ ...AUTH_ENV_KEYS,
200
+ ])
201
+ }
202
+
203
+ /** Build a Wrangler runtime environment without any Cloudflare authentication source. */
204
+ export function unauthenticatedEnvironment(environment = process.env) {
205
+ return pickEnvironment(environment, RUNTIME_ENV_KEYS)
206
+ }
207
+
208
+ /** Build an exact, secret-free Wrangler deployment command. */
209
+ export function wranglerDeployArgs({
210
+ deploymentId,
211
+ mode,
212
+ workerName,
213
+ secretsFile,
214
+ configFile,
215
+ profile,
216
+ temporary = false,
217
+ }) {
218
+ requireRuntimeMode(mode)
219
+ if (temporary && mode !== 'direct') {
220
+ throw new Error('Temporary accounts support only the Free direct runtime.')
221
+ }
222
+ const args = [
223
+ 'deploy',
224
+ '--env',
225
+ RUNTIME_MODES[mode].environment,
226
+ '--name',
227
+ workerName,
228
+ '--config',
229
+ configFile,
230
+ '--define',
231
+ `${DEPLOYMENT_ID_DEFINE}:${JSON.stringify(deploymentId)}`,
232
+ '--secrets-file',
233
+ secretsFile,
234
+ ]
235
+ if (temporary) args.push('--temporary')
236
+ if (profile !== undefined) args.push('--profile', profile)
237
+ return args
238
+ }
239
+
240
+ /** Parse the structured Wrangler deploy event and require a public HTTPS target. */
241
+ export function parseDeploymentOutput(source) {
242
+ const events = source.split(/\r?\n/u).filter(Boolean).map((line) => {
243
+ try {
244
+ return JSON.parse(line)
245
+ } catch {
246
+ throw new Error('Wrangler wrote malformed deployment metadata.')
247
+ }
248
+ })
249
+ const deploy = events.findLast(event => event?.type === 'deploy' && event?.version === 1)
250
+ if (deploy === undefined || !Array.isArray(deploy.targets)) {
251
+ throw new Error('Wrangler did not report a deployed Worker target.')
252
+ }
253
+ for (const target of deploy.targets) {
254
+ if (typeof target !== 'string') continue
255
+ const candidate = target.startsWith('https://') ? target : `https://${target}`
256
+ try {
257
+ const url = new URL(candidate)
258
+ if (url.protocol === 'https:' && url.username === '' && url.password === ''
259
+ && url.hostname.endsWith('.workers.dev')
260
+ && url.pathname === '/' && url.search === '' && url.hash === '') {
261
+ return { publicUrl: url.origin, versionId: deploy.version_id }
262
+ }
263
+ } catch {
264
+ // A route description is not necessarily a public URL; try the next target.
265
+ }
266
+ }
267
+ throw new Error('Wrangler did not report a public workers.dev URL for the Worker.')
268
+ }
269
+
270
+ export function parseClaimUrl(source) {
271
+ const match = CLAIM_URL.exec(source)
272
+ if (match === null) return undefined
273
+ const claimUrl = match[0].replace(/[),.;]+$/u, '')
274
+ return isTerminalSafe(claimUrl) ? claimUrl : undefined
275
+ }
276
+
277
+ /** Distinguish an absent Worker from account and transport failures. */
278
+ export function parseWorkerExistence(result) {
279
+ if (result.status === 0) return true
280
+ if (/\[code:\s*10007\]/u.test(`${result.stdout}\n${result.stderr}`)) return false
281
+ throw new Error(commandFailure('Could not check whether the Worker already exists', result))
282
+ }
283
+
284
+ /** Run the complete guided install with UI and Wrangler supplied as replaceable boundaries. */
285
+ export async function installEdge({
286
+ command = 'install',
287
+ ui,
288
+ runWrangler = executeWrangler,
289
+ deploymentId = randomUUID(),
290
+ environment = process.env,
291
+ createTemporaryDirectory = createPrivateTemporaryDirectory,
292
+ removePath = rm,
293
+ signal,
294
+ } = {}) {
295
+ if (ui === undefined) throw new Error('installEdge requires an installer UI.')
296
+ signal?.throwIfAborted()
297
+ let temporaryDirectory
298
+ let completedResult
299
+ let primaryError
300
+ if (command !== 'install' && command !== 'upgrade') throw new Error(`Unknown installer command: ${command}`)
301
+ ui.intro(`dsh-edge ${command}`)
302
+ try {
303
+ const mode = await ui.selectRuntime()
304
+ requireRuntimeMode(mode)
305
+
306
+ ui.step('Checking Cloudflare accounts…')
307
+ const detected = await detectAccounts({ runWrangler, environment, signal })
308
+ let accountSelection = await ui.selectAccount(accountChoices(mode, detected.accounts, command))
309
+ let profile
310
+ let profileEnvironment
311
+ let accounts = detected.accounts
312
+ if (accountSelection === 'login') {
313
+ ui.step('Opening Cloudflare sign-in…')
314
+ profileEnvironment = unauthenticatedEnvironment(environment)
315
+ const auth = await runWrangler(['auth', 'create', LOGIN_PROFILE], {
316
+ environment: profileEnvironment,
317
+ interactive: true,
318
+ signal,
319
+ })
320
+ requireSuccess(auth, 'Cloudflare sign-in failed')
321
+ profile = LOGIN_PROFILE
322
+ const signedIn = await requireAccounts({
323
+ runWrangler,
324
+ environment: profileEnvironment,
325
+ profile,
326
+ signal,
327
+ })
328
+ accounts = signedIn.accounts
329
+ accountSelection = await ui.selectAccount(accountChoices(mode, accounts, command)
330
+ .filter(choice => choice.value.startsWith('account:')))
331
+ }
332
+
333
+ const temporary = accountSelection === 'temporary'
334
+ if (temporary && command === 'upgrade') {
335
+ throw new Error('Temporary accounts cannot be upgraded before they are claimed.')
336
+ }
337
+ if (temporary && mode !== 'direct') {
338
+ throw new Error('Temporary accounts support only the Free direct runtime.')
339
+ }
340
+ const account = temporary
341
+ ? undefined
342
+ : requireSelectedAccount(accountSelection, accounts)
343
+
344
+ let workerName = DEFAULT_WORKER_NAME
345
+ while (true) {
346
+ workerName = await ui.workerName(workerName, validateWorkerName)
347
+ if (temporary) break
348
+ ui.step(`Checking ${workerName}…`)
349
+ const exists = parseWorkerExistence(await runWrangler([
350
+ 'deployments', 'list', '--name', workerName, '--json',
351
+ ...runtimeEnvironmentArgs(mode),
352
+ ...profileArgs(profile),
353
+ ], {
354
+ environment: accountEnvironment(profileEnvironment ?? environment, account.id),
355
+ signal,
356
+ }))
357
+ if (command === 'upgrade') {
358
+ if (!exists) throw new Error(`${workerName} does not exist in this account and runtime. Run dsh-edge install first.`)
359
+ break
360
+ }
361
+ if (!exists) break
362
+ const action = await ui.workerConflict(workerName)
363
+ if (action === 'update') break
364
+ if (action === 'cancel') throw new InstallCancelledError()
365
+ workerName = `${workerName}-2`
366
+ }
367
+
368
+ const secretMode = await ui.selectOwnerSecretMode()
369
+ const ownerSecret = secretMode === 'generate'
370
+ ? generateOwnerSecret()
371
+ : await ui.ownerSecret(validateOwnerSecret)
372
+ const secretError = validateOwnerSecret(ownerSecret)
373
+ if (secretError !== undefined) throw new Error(secretError)
374
+ const deepSeekKey = await ui.deepSeekKey(validateDeepSeekKey)
375
+ const deepSeekError = validateDeepSeekKey(deepSeekKey)
376
+ if (deepSeekError !== undefined) throw new Error(deepSeekError)
377
+
378
+ const confirmed = await ui.confirm({
379
+ mode,
380
+ modeLabel: RUNTIME_MODES[mode].label,
381
+ accountLabel: temporary ? 'Temporary account' : account.name,
382
+ workerName,
383
+ paid: mode === 'isolated',
384
+ })
385
+ if (!confirmed) throw new InstallCancelledError()
386
+ if (temporary && !await ui.acceptTemporaryTerms()) {
387
+ throw new InstallCancelledError()
388
+ }
389
+
390
+ temporaryDirectory = await createTemporaryDirectory()
391
+ const secretsFile = join(temporaryDirectory, 'secrets.json')
392
+ const configFile = join(temporaryDirectory, 'wrangler.json')
393
+ const outputFile = join(temporaryDirectory, 'wrangler-output.ndjson')
394
+ await writeModeWranglerConfig(mode, configFile)
395
+ await writeFile(secretsFile, JSON.stringify({
396
+ DEEPSEEK_API_KEY: deepSeekKey,
397
+ DSH_EDGE_ACCESS_KEY: ownerSecret,
398
+ }), { encoding: 'utf8', mode: 0o600, flag: 'wx' })
399
+
400
+ ui.step(command === 'upgrade' ? 'Upgrading the Worker…' : 'Installing the Worker…')
401
+ const commandEnvironment = temporary
402
+ ? unauthenticatedEnvironment(environment)
403
+ : accountEnvironment(profileEnvironment ?? environment, account.id)
404
+ if (temporary) commandEnvironment.XDG_CONFIG_HOME = temporaryDirectory
405
+ commandEnvironment.WRANGLER_LOG_SANITIZE = 'true'
406
+ commandEnvironment.WRANGLER_OUTPUT_FILE_PATH = outputFile
407
+ commandEnvironment.FORCE_COLOR = '0'
408
+ let deployResult
409
+ let credentialCleanupError
410
+ try {
411
+ deployResult = await runWrangler(wranglerDeployArgs({
412
+ deploymentId,
413
+ mode,
414
+ workerName,
415
+ secretsFile,
416
+ configFile,
417
+ profile: temporary ? undefined : profile,
418
+ temporary,
419
+ }), {
420
+ environment: commandEnvironment,
421
+ interactive: true,
422
+ capture: true,
423
+ signal,
424
+ })
425
+ } finally {
426
+ try {
427
+ await removePath(secretsFile, { force: true })
428
+ } catch (error) {
429
+ credentialCleanupError = error
430
+ }
431
+ }
432
+ if (deployResult.status !== 0) {
433
+ const cleanupDetail = credentialCleanupError === undefined
434
+ ? ''
435
+ : `\nTemporary credential cleanup also failed: ${describeError(credentialCleanupError)}`
436
+ throw new Error(`${formatDeployFailure(mode, deployResult)}${cleanupDetail}`)
437
+ }
438
+
439
+ const claimUrl = temporary
440
+ ? parseClaimUrl(`${deployResult.stdout}\n${deployResult.stderr}`)
441
+ : undefined
442
+ const recoverUploadedWorker = () => {
443
+ const recovery = { claimUrl, ownerSecret, workerName }
444
+ if (deployResult.outputFailure !== undefined) {
445
+ ui.outputFailureRecovery(recovery, deployResult.outputFailure.stream)
446
+ } else {
447
+ ui.recovery(recovery)
448
+ }
449
+ }
450
+ if (credentialCleanupError !== undefined) {
451
+ recoverUploadedWorker()
452
+ throw new Error(
453
+ `Could not remove temporary credentials: ${describeError(credentialCleanupError)}`,
454
+ { cause: credentialCleanupError },
455
+ )
456
+ }
457
+ if (deployResult.interrupted === true) {
458
+ recoverUploadedWorker()
459
+ throw abortReason(signal, 'Wrangler command interrupted after upload.')
460
+ }
461
+ if (deployResult.outputFailure !== undefined) {
462
+ recoverUploadedWorker()
463
+ throw deployResult.outputFailure
464
+ }
465
+ let deployment
466
+ try {
467
+ const output = await readBoundedUtf8File(outputFile, MAX_CAPTURE_BYTES)
468
+ deployment = parseDeploymentOutput(output)
469
+ signal?.throwIfAborted()
470
+ if (temporary && claimUrl === undefined) {
471
+ throw new Error('The temporary Worker was uploaded, but Wrangler did not return its claim URL.')
472
+ }
473
+ } catch (error) {
474
+ ui.recovery({
475
+ claimUrl,
476
+ ownerSecret,
477
+ publicUrl: deployment?.publicUrl,
478
+ workerName,
479
+ })
480
+ throw error
481
+ }
482
+ const result = {
483
+ ...deployment,
484
+ account,
485
+ claimUrl,
486
+ deploymentId,
487
+ mode,
488
+ ownerSecret,
489
+ temporary,
490
+ workerName,
491
+ }
492
+ completedResult = result
493
+ } catch (error) {
494
+ primaryError = signal?.aborted ? abortReason(signal, 'Installation interrupted.') : error
495
+ }
496
+
497
+ if (primaryError === undefined && signal?.aborted) {
498
+ primaryError = abortReason(signal, 'Installation interrupted.')
499
+ if (completedResult !== undefined) ui.recovery(completedResult)
500
+ }
501
+ let directoryCleanupError
502
+ if (temporaryDirectory !== undefined) {
503
+ try {
504
+ await removePath(temporaryDirectory, { recursive: true, force: true })
505
+ } catch (error) {
506
+ directoryCleanupError = error
507
+ }
508
+ }
509
+ if (primaryError === undefined && signal?.aborted) {
510
+ primaryError = abortReason(signal, 'Installation interrupted.')
511
+ if (completedResult !== undefined) ui.recovery(completedResult)
512
+ }
513
+ if (directoryCleanupError !== undefined) {
514
+ const message = `Could not remove private temporary files: ${describeError(directoryCleanupError)}`
515
+ if (primaryError !== undefined) {
516
+ ui.cleanupFailure(message)
517
+ } else {
518
+ if (completedResult !== undefined) ui.recovery(completedResult)
519
+ primaryError = new Error(message, { cause: directoryCleanupError })
520
+ }
521
+ }
522
+ if (primaryError !== undefined) throw primaryError
523
+ if (completedResult === undefined) throw new Error('Installation ended without a result.')
524
+ ui.success(completedResult)
525
+ return completedResult
526
+ }
527
+
528
+ export function wranglerProcessInvocation(args, {
529
+ nodeExecutable = process.execPath,
530
+ wranglerCli = WRANGLER_CLI,
531
+ } = {}) {
532
+ return { command: nodeExecutable, args: [wranglerCli, ...args] }
533
+ }
534
+
535
+ export async function executeWrangler(args, {
536
+ environment = wranglerEnvironment(),
537
+ interactive = false,
538
+ capture = true,
539
+ forceKillAfterDelay = 2_000,
540
+ invocation = wranglerProcessInvocation(args),
541
+ signal,
542
+ stderrDestination = process.stderr,
543
+ stdoutDestination = process.stdout,
544
+ } = {}) {
545
+ signal?.throwIfAborted()
546
+ if (!Number.isFinite(forceKillAfterDelay) || forceKillAfterDelay < 0) {
547
+ throw new Error('forceKillAfterDelay must be a non-negative finite number.')
548
+ }
549
+ const child = execa(invocation.command, invocation.args, {
550
+ buffer: false,
551
+ cleanup: true,
552
+ cwd: appDirectory,
553
+ env: environment,
554
+ extendEnv: false,
555
+ forceKillAfterDelay,
556
+ killDescendants: true,
557
+ reject: false,
558
+ stderr: 'pipe',
559
+ stdin: interactive ? 'inherit' : 'ignore',
560
+ stdout: 'pipe',
561
+ })
562
+ let stdout = ''
563
+ let stderr = ''
564
+ let outputFailure
565
+ let termination
566
+ let stdoutForwarder
567
+ let stderrForwarder
568
+ const terminate = () => {
569
+ termination ??= terminateProcessTree(child, forceKillAfterDelay)
570
+ }
571
+ const failOutput = (streamName, error) => {
572
+ outputFailure ??= new InstallerOutputError(streamName, error)
573
+ terminate()
574
+ stdoutForwarder?.cancel()
575
+ stderrForwarder?.cancel()
576
+ }
577
+ const stdoutTerminal = createTerminalSanitizer()
578
+ const stderrTerminal = createTerminalSanitizer()
579
+ stdoutForwarder = interactive && child.stdout
580
+ ? createOutputForwarder(child.stdout, stdoutDestination, error => failOutput('stdout', error))
581
+ : undefined
582
+ stderrForwarder = interactive && child.stderr
583
+ ? createOutputForwarder(child.stderr, stderrDestination, error => failOutput('stderr', error))
584
+ : undefined
585
+ child.stdout?.setEncoding('utf8')
586
+ child.stderr?.setEncoding('utf8')
587
+ child.stdout?.on('data', (chunk) => {
588
+ const terminalChunk = interactive ? stdoutTerminal.push(chunk) : ''
589
+ if (terminalChunk !== '') stdoutForwarder?.write(terminalChunk)
590
+ if (capture) stdout = appendBounded(stdout, chunk)
591
+ })
592
+ child.stderr?.on('data', (chunk) => {
593
+ const terminalChunk = interactive ? stderrTerminal.push(chunk) : ''
594
+ if (terminalChunk !== '') stderrForwarder?.write(terminalChunk)
595
+ if (capture) stderr = appendBounded(stderr, chunk)
596
+ })
597
+ const interrupt = () => {
598
+ terminate()
599
+ stdoutForwarder?.cancel()
600
+ stderrForwarder?.cancel()
601
+ }
602
+ signal?.addEventListener('abort', interrupt, { once: true })
603
+ if (signal?.aborted) interrupt()
604
+ let result
605
+ try {
606
+ result = await child
607
+ await termination
608
+ await Promise.all([
609
+ stdoutForwarder?.settled(),
610
+ stderrForwarder?.settled(),
611
+ ])
612
+ await termination
613
+ } finally {
614
+ signal?.removeEventListener('abort', interrupt)
615
+ stdoutForwarder?.dispose()
616
+ stderrForwarder?.dispose()
617
+ await termination
618
+ }
619
+ const processError = result.failed && !result.isCanceled
620
+ && result.exitCode === undefined && result.signal === undefined
621
+ ? result
622
+ : undefined
623
+ return resolveWranglerClose({
624
+ outputFailure,
625
+ processError,
626
+ signal,
627
+ status: result.exitCode ?? null,
628
+ stderr,
629
+ stdout,
630
+ })
631
+ }
632
+
633
+ async function terminateProcessTree(child, graceMs) {
634
+ const pid = child.pid
635
+ if (pid === undefined) return
636
+ if (process.platform === 'win32') {
637
+ await terminateWindowsProcessTree(child, pid)
638
+ return
639
+ }
640
+ signalProcessGroup(pid, 'SIGTERM', child)
641
+ if (await waitForProcessGroupExit(pid, graceMs)) return
642
+ signalProcessGroup(pid, 'SIGKILL', child)
643
+ if (!await waitForProcessGroupExit(pid, Math.max(graceMs, 100))) {
644
+ throw new Error(`Wrangler process group ${pid} did not stop after SIGKILL.`)
645
+ }
646
+ }
647
+
648
+ function signalProcessGroup(pid, signal, child) {
649
+ try {
650
+ process.kill(-pid, signal)
651
+ } catch {
652
+ child.kill(signal)
653
+ }
654
+ }
655
+
656
+ async function waitForProcessGroupExit(pid, timeoutMs) {
657
+ const deadline = Date.now() + timeoutMs
658
+ while (processGroupExists(pid)) {
659
+ if (Date.now() >= deadline) return false
660
+ await sleep(Math.min(15, Math.max(1, deadline - Date.now())))
661
+ }
662
+ return true
663
+ }
664
+
665
+ function processGroupExists(pid) {
666
+ try {
667
+ process.kill(-pid, 0)
668
+ return true
669
+ } catch (error) {
670
+ if (error?.code === 'ESRCH') return false
671
+ throw error
672
+ }
673
+ }
674
+
675
+ async function terminateWindowsProcessTree(child, pid) {
676
+ const windowsRoot = process.env.SystemRoot ?? process.env.windir
677
+ const taskkill = typeof windowsRoot === 'string' && /^[a-z]:[\\/]/iu.test(windowsRoot)
678
+ ? join(windowsRoot, 'System32', 'taskkill.exe')
679
+ : 'taskkill.exe'
680
+ const succeeded = await new Promise(resolve => {
681
+ execFile(taskkill, ['/PID', String(pid), '/T', '/F'], { windowsHide: true }, (error) => {
682
+ resolve(error === null)
683
+ })
684
+ })
685
+ if (!succeeded) child.kill('SIGKILL')
686
+ }
687
+
688
+ /** Keep a successful child exit distinct from a concurrent abort request. */
689
+ export function resolveWranglerClose({ outputFailure, processError, signal, status, stderr, stdout }) {
690
+ if (processError !== undefined && !isAbortProcessError(processError, signal)) {
691
+ throw processError
692
+ }
693
+ if (signal?.aborted) {
694
+ if (status === 0) {
695
+ return {
696
+ interrupted: true,
697
+ ...(outputFailure === undefined ? {} : { outputFailure }),
698
+ status,
699
+ stderr,
700
+ stdout,
701
+ }
702
+ }
703
+ throw abortReason(signal, 'Wrangler command interrupted.')
704
+ }
705
+ if (outputFailure !== undefined) {
706
+ if (status === 0) return { outputFailure, status, stderr, stdout }
707
+ throw outputFailure
708
+ }
709
+ return { status, stderr, stdout }
710
+ }
711
+
712
+ /** Forward a child stream with backpressure and a recoverable destination-failure boundary. */
713
+ export function createOutputForwarder(source, destination, onFailure) {
714
+ let failed = false
715
+ let pendingWrites = 0
716
+ let waitingForDrain = false
717
+ const settleWaiters = new Set()
718
+ const settle = () => {
719
+ if (pendingWrites !== 0) return
720
+ for (const resolve of settleWaiters) resolve()
721
+ settleWaiters.clear()
722
+ }
723
+ const resume = () => {
724
+ waitingForDrain = false
725
+ if (!failed) source.resume()
726
+ }
727
+ const fail = (error) => {
728
+ if (failed) return
729
+ failed = true
730
+ source.pause()
731
+ if (waitingForDrain) destination.removeListener('drain', resume)
732
+ pendingWrites = 0
733
+ settle()
734
+ onFailure(error instanceof Error ? error : new Error(String(error)))
735
+ }
736
+ const closed = () => fail(new Error('Output destination closed before Wrangler completed.'))
737
+ const cancel = () => {
738
+ if (failed) return
739
+ failed = true
740
+ source.pause()
741
+ if (waitingForDrain) destination.removeListener('drain', resume)
742
+ pendingWrites = 0
743
+ settle()
744
+ }
745
+ destination.on('error', fail)
746
+ destination.on('close', closed)
747
+ return {
748
+ write(chunk) {
749
+ if (failed) return
750
+ pendingWrites += 1
751
+ let completed = false
752
+ const written = (error) => {
753
+ if (completed) return
754
+ completed = true
755
+ pendingWrites = Math.max(0, pendingWrites - 1)
756
+ if (error !== undefined && error !== null) fail(error)
757
+ settle()
758
+ }
759
+ try {
760
+ if (destination.write(chunk, written) || waitingForDrain || completed) return
761
+ waitingForDrain = true
762
+ source.pause()
763
+ destination.once('drain', resume)
764
+ } catch (error) {
765
+ written(error)
766
+ }
767
+ },
768
+ settled() {
769
+ if (pendingWrites === 0) return Promise.resolve()
770
+ return new Promise(resolve => settleWaiters.add(resolve))
771
+ },
772
+ cancel,
773
+ dispose() {
774
+ cancel()
775
+ destination.removeListener('error', fail)
776
+ destination.removeListener('close', closed)
777
+ },
778
+ }
779
+ }
780
+
781
+ function isTerminalSafe(value) {
782
+ return !CONTROL_CHARACTER.test(value) && !BIDI_CONTROL.test(value)
783
+ }
784
+
785
+ /** Create a chunk-safe terminal filter that retains plain text only. */
786
+ export function createTerminalSanitizer() {
787
+ let state = 'text'
788
+ return {
789
+ push(chunk) {
790
+ let output = ''
791
+ for (const character of chunk) {
792
+ const code = character.codePointAt(0)
793
+ if (state === 'osc' || state === 'string') {
794
+ if (character === '\u009C' || (state === 'osc' && character === '\u0007')) {
795
+ state = 'text'
796
+ } else if (character === '\u001B') {
797
+ state = `${state}-escape`
798
+ }
799
+ continue
800
+ }
801
+ if (state === 'osc-escape' || state === 'string-escape') {
802
+ if (character === '\\' || character === '\u009C') state = 'text'
803
+ else if (character !== '\u001B') state = state.startsWith('osc') ? 'osc' : 'string'
804
+ continue
805
+ }
806
+ if (state === 'escape') {
807
+ if (character === '[') {
808
+ state = 'csi'
809
+ } else if (character === ']') {
810
+ state = 'osc'
811
+ } else if ('PX^_'.includes(character)) {
812
+ state = 'string'
813
+ } else if (character !== '\u001B') {
814
+ state = 'text'
815
+ }
816
+ continue
817
+ }
818
+ if (state === 'csi') {
819
+ if (code >= 0x40 && code <= 0x7E) {
820
+ state = 'text'
821
+ }
822
+ continue
823
+ }
824
+ if (character === '\u001B') {
825
+ state = 'escape'
826
+ } else if (character === '\u009B') {
827
+ state = 'csi'
828
+ } else if (character === '\u009D') {
829
+ state = 'osc'
830
+ } else if (character === '\u0090' || character === '\u0098'
831
+ || character === '\u009E' || character === '\u009F') {
832
+ state = 'string'
833
+ } else if (!BIDI_CONTROL.test(character) && (character === '\t' || character === '\n'
834
+ || character === '\r' || (code >= 0x20 && code <= 0x7E) || code >= 0xA0)) {
835
+ output += character
836
+ }
837
+ }
838
+ return output
839
+ },
840
+ }
841
+ }
842
+
843
+ function abortReason(signal, fallbackMessage) {
844
+ return signal?.reason instanceof Error ? signal.reason : new Error(fallbackMessage)
845
+ }
846
+
847
+ function isAbortProcessError(error, signal) {
848
+ return signal?.aborted === true
849
+ && error !== null
850
+ && typeof error === 'object'
851
+ && 'name' in error
852
+ && error.name === 'AbortError'
853
+ }
854
+
855
+ async function detectAccounts(options) {
856
+ const result = await options.runWrangler(['whoami', '--json'], {
857
+ environment: wranglerEnvironment(options.environment),
858
+ signal: options.signal,
859
+ })
860
+ try {
861
+ const value = JSON.parse(result.stdout)
862
+ if (value !== null && typeof value === 'object' && value.loggedIn === false) {
863
+ return { accounts: [] }
864
+ }
865
+ } catch {
866
+ // A nonzero command failure below preserves Wrangler's actionable diagnostic.
867
+ }
868
+ if (result.status !== 0) {
869
+ throw new Error(commandFailure('Could not check Cloudflare authentication', result))
870
+ }
871
+ return parseWhoami(result.stdout)
872
+ }
873
+
874
+ async function requireAccounts({ runWrangler, environment, profile, signal }) {
875
+ const result = await runWrangler(['whoami', '--json', ...profileArgs(profile)], {
876
+ environment,
877
+ signal,
878
+ })
879
+ requireSuccess(result, 'Could not read the signed-in Cloudflare account')
880
+ const whoami = parseWhoami(result.stdout)
881
+ if (whoami.accounts.length === 0) throw new Error('The Cloudflare login has no available accounts.')
882
+ return whoami
883
+ }
884
+
885
+ function requireSelectedAccount(selection, accounts) {
886
+ const id = selection.startsWith('account:') ? selection.slice('account:'.length) : ''
887
+ const account = accounts.find(candidate => candidate.id === id)
888
+ if (account === undefined) throw new Error('Select an available Cloudflare account.')
889
+ return account
890
+ }
891
+
892
+ function profileArgs(profile) {
893
+ return profile === undefined ? [] : ['--profile', profile]
894
+ }
895
+
896
+ function runtimeEnvironmentArgs(mode) {
897
+ const target = RUNTIME_MODES[mode].environment
898
+ return target === '' ? [] : ['--env', target]
899
+ }
900
+
901
+ function accountEnvironment(environment, accountId) {
902
+ return { ...wranglerEnvironment(environment), CLOUDFLARE_ACCOUNT_ID: accountId }
903
+ }
904
+
905
+ function pickEnvironment(environment, keys) {
906
+ const result = {}
907
+ for (const key of keys) {
908
+ const value = environment[key]
909
+ if (value !== undefined) result[key] = value
910
+ }
911
+ for (const [key, value] of Object.entries(environment)) {
912
+ if (key.startsWith('LC_') && !SENSITIVE_ENV_KEY.test(key) && value !== undefined) {
913
+ result[key] = value
914
+ }
915
+ }
916
+ return result
917
+ }
918
+
919
+ function requireRuntimeMode(mode) {
920
+ if (!Object.hasOwn(RUNTIME_MODES, mode)) {
921
+ throw new Error(`Unsupported runtime mode: ${String(mode)}`)
922
+ }
923
+ }
924
+
925
+ function requireSuccess(result, prefix) {
926
+ if (result.outputFailure !== undefined) throw result.outputFailure
927
+ if (result.status !== 0) throw new Error(commandFailure(prefix, result))
928
+ }
929
+
930
+ function commandFailure(prefix, result) {
931
+ const detail = stripAnsi(result.stderr || result.stdout).trim()
932
+ return detail === '' ? `${prefix}.` : `${prefix}: ${detail}`
933
+ }
934
+
935
+ function describeError(error) {
936
+ return error instanceof Error ? error.message : String(error)
937
+ }
938
+
939
+ function formatDeployFailure(mode, result) {
940
+ const failure = commandFailure('Cloudflare installation failed', result)
941
+ if (mode !== 'isolated') return failure
942
+ return `${failure}\nThe isolated runtime requires the Workers Paid plan (starting at $5/month). `
943
+ + 'Enable Workers Paid for this account or install the Free direct runtime.'
944
+ }
945
+
946
+ function stripAnsi(value) {
947
+ return createTerminalSanitizer().push(value)
948
+ }
949
+
950
+ function appendBounded(current, chunk) {
951
+ return truncateUtf8Tail(current + chunk, MAX_CAPTURE_BYTES)
952
+ }
953
+
954
+ /** Retain at most maxBytes from the end of a string without splitting UTF-8. */
955
+ export function truncateUtf8Tail(value, maxBytes) {
956
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
957
+ throw new TypeError('maxBytes must be a non-negative safe integer.')
958
+ }
959
+ const bytes = Buffer.from(value, 'utf8')
960
+ if (bytes.byteLength <= maxBytes) return value
961
+ let start = bytes.byteLength - maxBytes
962
+ while (start < bytes.byteLength && (bytes[start] & 0xC0) === 0x80) start += 1
963
+ return bytes.toString('utf8', start)
964
+ }
965
+
966
+ async function readBoundedUtf8File(path, maxBytes) {
967
+ const file = await open(path, 'r')
968
+ try {
969
+ const bytes = Buffer.allocUnsafe(maxBytes + 1)
970
+ let offset = 0
971
+ while (offset < bytes.byteLength) {
972
+ const result = await file.read(bytes, offset, bytes.byteLength - offset, null)
973
+ if (result.bytesRead === 0) break
974
+ offset += result.bytesRead
975
+ }
976
+ if (offset > maxBytes) {
977
+ throw new Error(`Wrangler deployment metadata exceeded ${maxBytes} UTF-8 bytes.`)
978
+ }
979
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, offset))
980
+ } finally {
981
+ await file.close()
982
+ }
983
+ }
984
+
985
+ async function createPrivateTemporaryDirectory() {
986
+ const directory = await mkdtemp(join(tmpdir(), 'dsh-edge-install-'))
987
+ await chmod(directory, 0o700)
988
+ return directory
989
+ }