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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +183 -0
- package/README.zh.md +183 -0
- package/THIRD_PARTY_NOTICES.md +199 -0
- package/dist/_headers +3 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-C-1AiF3k.js +112 -0
- package/dist/assets/index-CSGf6Qzd.css +1 -0
- package/dist/assets/langs/c-BIGW1oBm.js +2 -0
- package/dist/assets/langs/cpp-DIPi6g--.js +2 -0
- package/dist/assets/langs/csharp-DSvCPggb.js +2 -0
- package/dist/assets/langs/css-CLj8gQPS.js +2 -0
- package/dist/assets/langs/go-C27-OAKa.js +2 -0
- package/dist/assets/langs/html-CfGypltT.js +2 -0
- package/dist/assets/langs/ini-BEwlwnbL.js +2 -0
- package/dist/assets/langs/java-CylS5w8V.js +2 -0
- package/dist/assets/langs/kotlin-BdnUsdx6.js +2 -0
- package/dist/assets/langs/less-B1dDrJ26.js +2 -0
- package/dist/assets/langs/lua-BaeVxFsk.js +2 -0
- package/dist/assets/langs/markdown-Cvjx9yec.js +2 -0
- package/dist/assets/langs/mdx-Cmh6b_Ma.js +2 -0
- package/dist/assets/langs/php-D-uCvoST.js +2 -0
- package/dist/assets/langs/python-B6aJPvgy.js +2 -0
- package/dist/assets/langs/ruby-DEItuUFs.js +2 -0
- package/dist/assets/langs/rust-B1yitclQ.js +2 -0
- package/dist/assets/langs/scss-D5BDwBP9.js +2 -0
- package/dist/assets/langs/sql-CRqJ_cUM.js +2 -0
- package/dist/assets/langs/swift-D82vCrfD.js +2 -0
- package/dist/assets/langs/toml-vGWfd6FD.js +2 -0
- package/dist/assets/langs/xml-sdJ4AIDG.js +2 -0
- package/dist/assets/langs/yaml-Buea-lGh.js +2 -0
- package/dist/assets/vendor-Cjbwl5VI.js +411 -0
- package/dist/assets/vendor-CjyC-hUb.css +1 -0
- package/dist/favicon.svg +8 -0
- package/dist/index.html +35 -0
- package/dist/manifest.webmanifest +16 -0
- package/dist/plugins/@deepseek-ai/dsh-api-gateway/client.js +420 -0
- package/dist/plugins/@deepseek-ai/dsh-api-remotes/client.js +5937 -0
- package/dist/plugins/@deepseek-ai/dsh-client-connection/client.js +10207 -0
- package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +1231 -0
- package/dist/plugins/@deepseek-ai/dsh-client-modules/client.js +259 -0
- package/dist/plugins/@deepseek-ai/dsh-client-runtime/client.js +10539 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +1725 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +1116 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +9832 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +373 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-edge/client.js +549 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +859 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +280 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +456 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +804 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +461 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings/client.js +254 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +609 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +292 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +327 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +710 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +1312 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +1630 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +7370 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +700 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +474 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +2426 -0
- package/dist/plugins/@deepseek-ai/dsh-typert-registry/client.js +1372 -0
- package/package.json +97 -0
- package/scripts/assemble-web.mjs +230 -0
- package/scripts/bundle-size.d.mts +2 -0
- package/scripts/bundle-size.mjs +26 -0
- package/scripts/bundle.mjs +53 -0
- package/scripts/cli.d.mts +39 -0
- package/scripts/cli.mjs +496 -0
- package/scripts/install.d.mts +149 -0
- package/scripts/install.mjs +989 -0
- package/scripts/legal-files.mjs +24 -0
- package/scripts/verify-packed.mjs +32 -0
- package/scripts/wrangler-config.d.mts +18 -0
- package/scripts/wrangler-config.mjs +84 -0
- package/src/agent.ts +119 -0
- package/src/auth.ts +352 -0
- package/src/deepseek.ts +231 -0
- package/src/deployment.ts +147 -0
- package/src/direct-shell-core-empty.ts +10 -0
- package/src/direct-shell-protocol.ts +5 -0
- package/src/direct-shell.ts +341 -0
- package/src/do-session-persistence.ts +1064 -0
- package/src/edge-api.ts +796 -0
- package/src/edge-credentials.ts +56 -0
- package/src/edge-remotes.ts +41 -0
- package/src/edge-workspace-store.ts +339 -0
- package/src/http.ts +212 -0
- package/src/index.ts +196 -0
- package/src/instance.ts +1007 -0
- package/src/isolated-direct-shell-unavailable.ts +11 -0
- package/src/protocol.ts +39 -0
- package/src/release.ts +9 -0
- package/src/session-store.ts +1110 -0
- package/src/sse.ts +86 -0
- package/src/web-search.ts +38 -0
- package/src/workspace.ts +270 -0
- 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
|
+
}
|