dsh-edge 0.1.3 → 0.2.0-alpha.2
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/README.i18n.yaml +6 -6
- package/README.md +25 -10
- package/README.zh.md +25 -10
- package/THIRD_PARTY_NOTICES.md +10019 -187
- package/dist/index.html +1 -1
- package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +7 -7
- package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +61 -61
- package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +14 -14
- package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +287 -287
- package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +10 -10
- package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +11 -11
- package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +15 -15
- package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +8 -8
- package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +27 -27
- package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +8 -8
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +31 -31
- package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +23 -23
- package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +19 -19
- package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +29 -29
- package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +7 -7
- package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +64 -64
- package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +183 -183
- package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +44 -44
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +24 -24
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +79 -79
- package/dist/plugins/dsh-edge-client-ui/client.js +68 -53
- package/package.json +22 -22
- package/scripts/activation.d.mts +34 -0
- package/scripts/activation.mjs +133 -0
- package/scripts/cli.d.mts +9 -0
- package/scripts/cli.mjs +66 -3
- package/scripts/dev.mjs +38 -0
- package/scripts/install.d.mts +8 -0
- package/scripts/install.mjs +95 -4
- package/scripts/legal-files.d.mts +23 -0
- package/scripts/legal-files.mjs +222 -40
- package/scripts/pnpm-invocation.d.mts +14 -0
- package/scripts/pnpm-invocation.mjs +25 -0
- package/scripts/publish.d.mts +25 -0
- package/scripts/publish.mjs +145 -0
- package/scripts/smoke-installed.mjs +27 -22
- package/scripts/verify-packed.mjs +8 -0
- package/scripts/wrangler-config-core.mjs +107 -0
- package/scripts/wrangler-config.d.mts +2 -0
- package/scripts/wrangler-config.mjs +9 -78
- package/worker/direct/index.js +1136 -1139
- package/worker/isolated/index.js +872 -875
- package/scripts/assemble-web.mjs +0 -230
- package/scripts/bundle.mjs +0 -86
package/scripts/cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { realpathSync, writeSync } from 'node:fs'
|
|
|
5
5
|
import { Writable } from 'node:stream'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import edgePackage from '../package.json' with { type: 'json' }
|
|
8
|
+
import { observePublicActivation } from './activation.mjs'
|
|
8
9
|
import {
|
|
9
10
|
executeWrangler,
|
|
10
11
|
InstallCancelledError,
|
|
@@ -18,6 +19,12 @@ const INTERRUPT_EXIT_CODES = new Map([
|
|
|
18
19
|
['SIGTERM', 143],
|
|
19
20
|
])
|
|
20
21
|
const OUTPUT_DRAIN_TIMEOUT_MS = 1_000
|
|
22
|
+
const HERO_MIN_COLUMNS = 68
|
|
23
|
+
const DSH_EDGE_HERO = String.raw` ____ ____ _ _ _____ ____ ____ _____
|
|
24
|
+
| _ \/ ___|| | | | | ____| _ \ / ___| ____|
|
|
25
|
+
| | | \___ \| |_| |_____| _| | | | | | _| _|
|
|
26
|
+
| |_| |___) | _ |_____| |___| |_| | |_| | |___
|
|
27
|
+
|____/|____/|_| |_| |_____|____/ \____|_____|`
|
|
21
28
|
|
|
22
29
|
export class InstallInterruptedError extends InstallCancelledError {
|
|
23
30
|
constructor(signal) {
|
|
@@ -38,6 +45,25 @@ export function parseCommand(args) {
|
|
|
38
45
|
throw usageError()
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
export function renderInstallerIntro(command, {
|
|
49
|
+
columns = 80,
|
|
50
|
+
isTTY = false,
|
|
51
|
+
version = edgePackage.version,
|
|
52
|
+
upstreamVersion = edgePackage.dshEdge.upstreamVersion,
|
|
53
|
+
} = {}) {
|
|
54
|
+
if (!isTTY || columns < HERO_MIN_COLUMNS) {
|
|
55
|
+
return `dsh-edge ${command} · ${version} · Harness ${upstreamVersion}`
|
|
56
|
+
}
|
|
57
|
+
return [
|
|
58
|
+
'',
|
|
59
|
+
DSH_EDGE_HERO,
|
|
60
|
+
'',
|
|
61
|
+
'DeepSeek Harness on Cloudflare',
|
|
62
|
+
`dsh-edge ${version} · Harness ${upstreamVersion}`,
|
|
63
|
+
`community project · ${command}`,
|
|
64
|
+
].join('\n')
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
export function createInstallerUi(
|
|
42
68
|
clack = prompt,
|
|
43
69
|
signal,
|
|
@@ -54,8 +80,13 @@ export function createInstallerUi(
|
|
|
54
80
|
? clack.note(message, title)
|
|
55
81
|
: clack.note(message, title, { output })
|
|
56
82
|
let deploymentSpinner
|
|
83
|
+
let activationSpinner
|
|
84
|
+
const terminal = output ?? process.stdout
|
|
57
85
|
return {
|
|
58
|
-
intro:
|
|
86
|
+
intro: () => log(clack.intro, renderInstallerIntro(command, {
|
|
87
|
+
columns: terminal.columns ?? 80,
|
|
88
|
+
isTTY: terminal.isTTY === true,
|
|
89
|
+
})),
|
|
59
90
|
step: message => log(clack.log.step, message),
|
|
60
91
|
async selectRuntime() {
|
|
61
92
|
return await requireAnswer(await clack.select(withOutput({
|
|
@@ -164,6 +195,20 @@ export function createInstallerUi(
|
|
|
164
195
|
else deploymentSpinner.error('Cloudflare did not accept the Worker upload.')
|
|
165
196
|
deploymentSpinner = undefined
|
|
166
197
|
},
|
|
198
|
+
activationStart(message) {
|
|
199
|
+
activationSpinner = clack.spinner(withOutput({ indicator: 'timer', signal }))
|
|
200
|
+
activationSpinner.start(message)
|
|
201
|
+
},
|
|
202
|
+
activationFinish(result) {
|
|
203
|
+
if (activationSpinner === undefined) return
|
|
204
|
+
if (result?.status === 'ready') activationSpinner.stop('Public URL is ready.')
|
|
205
|
+
else if (result?.status === 'pending') {
|
|
206
|
+
activationSpinner.stop('Worker uploaded; public URL activation is still pending.')
|
|
207
|
+
} else {
|
|
208
|
+
activationSpinner.stop('Stopped waiting for public URL activation.')
|
|
209
|
+
}
|
|
210
|
+
activationSpinner = undefined
|
|
211
|
+
},
|
|
167
212
|
failedDeployment(result) {
|
|
168
213
|
note([
|
|
169
214
|
`Worker: ${result.workerName}`,
|
|
@@ -193,6 +238,14 @@ export function createInstallerUi(
|
|
|
193
238
|
},
|
|
194
239
|
success(result) {
|
|
195
240
|
const lines = [
|
|
241
|
+
...(result.activation?.status === 'ready'
|
|
242
|
+
? ['Status: Ready']
|
|
243
|
+
: [
|
|
244
|
+
'Status: Cloudflare is still activating the public URL.',
|
|
245
|
+
'First-time workers.dev activation can take about a minute.',
|
|
246
|
+
'If the URL shows a placeholder, wait a moment and refresh.',
|
|
247
|
+
]),
|
|
248
|
+
'',
|
|
196
249
|
`URL: ${result.publicUrl}`,
|
|
197
250
|
`Owner access key: ${result.ownerSecret}`,
|
|
198
251
|
]
|
|
@@ -204,6 +257,7 @@ export function createInstallerUi(
|
|
|
204
257
|
'1. Claim this temporary account within 60 minutes to keep the Worker and its data.',
|
|
205
258
|
'2. Open the URL above.',
|
|
206
259
|
'3. Enter the owner access key when prompted.',
|
|
260
|
+
'4. Save the owner access key for future upgrades.',
|
|
207
261
|
)
|
|
208
262
|
} else {
|
|
209
263
|
lines.push(
|
|
@@ -211,10 +265,18 @@ export function createInstallerUi(
|
|
|
211
265
|
'Next steps:',
|
|
212
266
|
'1. Open the URL above.',
|
|
213
267
|
'2. Enter the owner access key when prompted.',
|
|
268
|
+
'3. Save the owner access key for future upgrades.',
|
|
214
269
|
)
|
|
215
270
|
}
|
|
216
|
-
|
|
217
|
-
|
|
271
|
+
const ready = result.activation?.status === 'ready'
|
|
272
|
+
const title = ready
|
|
273
|
+
? (command === 'upgrade' ? 'dsh-edge upgrade is live' : 'dsh-edge is ready')
|
|
274
|
+
: 'Worker uploaded — activation pending'
|
|
275
|
+
note(lines.join('\n'), title)
|
|
276
|
+
const outro = ready
|
|
277
|
+
? (command === 'upgrade' ? 'Your dsh-edge upgrade is live.' : 'Your dsh-edge is ready.')
|
|
278
|
+
: `${command === 'upgrade' ? 'Upgrade' : 'Installation'} succeeded; Cloudflare is still activating the public URL.`
|
|
279
|
+
log(clack.outro, outro)
|
|
218
280
|
},
|
|
219
281
|
}
|
|
220
282
|
}
|
|
@@ -294,6 +356,7 @@ export async function runInstaller({
|
|
|
294
356
|
controller.signal.addEventListener('abort', cancelBlockedOutput)
|
|
295
357
|
const installOperation = install ?? (options => installEdgeImpl({
|
|
296
358
|
...options,
|
|
359
|
+
observeActivation: observePublicActivation,
|
|
297
360
|
runWrangler: (args, runOptions) => wranglerRunner(args, {
|
|
298
361
|
...runOptions,
|
|
299
362
|
forwardOutput: verbose || (runOptions?.forwardOutput ?? runOptions?.interactive ?? false),
|
package/scripts/dev.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Serve the exact prebuilt artifact used by the installer. */
|
|
2
|
+
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { randomUUID } from 'node:crypto'
|
|
5
|
+
import { rm } from 'node:fs/promises'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { execa } from 'execa'
|
|
9
|
+
import { writePrebuiltModeWranglerConfig } from './wrangler-config.mjs'
|
|
10
|
+
|
|
11
|
+
const mode = process.argv[2]
|
|
12
|
+
if (mode !== 'direct' && mode !== 'isolated') {
|
|
13
|
+
process.stderr.write('Usage: node scripts/dev.mjs <direct|isolated> [wrangler dev options]\n')
|
|
14
|
+
process.exitCode = 2
|
|
15
|
+
} else {
|
|
16
|
+
const appDirectory = fileURLToPath(new URL('..', import.meta.url))
|
|
17
|
+
const require = createRequire(import.meta.url)
|
|
18
|
+
// Keep the generated config beside `.dev.vars`; Wrangler resolves local
|
|
19
|
+
// secrets relative to the configuration file rather than the process cwd.
|
|
20
|
+
const configFile = join(appDirectory, `.wrangler.dev.${randomUUID()}.json`)
|
|
21
|
+
try {
|
|
22
|
+
await writePrebuiltModeWranglerConfig(mode, configFile)
|
|
23
|
+
const result = await execa(process.execPath, [
|
|
24
|
+
require.resolve('wrangler'),
|
|
25
|
+
'dev',
|
|
26
|
+
'--config', configFile,
|
|
27
|
+
'--env', mode === 'direct' ? '' : 'isolated',
|
|
28
|
+
...process.argv.slice(3),
|
|
29
|
+
], {
|
|
30
|
+
cwd: appDirectory,
|
|
31
|
+
stdio: 'inherit',
|
|
32
|
+
reject: false,
|
|
33
|
+
})
|
|
34
|
+
process.exitCode = result.exitCode ?? 1
|
|
35
|
+
} finally {
|
|
36
|
+
await rm(configFile, { force: true })
|
|
37
|
+
}
|
|
38
|
+
}
|
package/scripts/install.d.mts
CHANGED
|
@@ -34,6 +34,8 @@ export interface InstallerUi {
|
|
|
34
34
|
acceptTemporaryTerms(): Promise<boolean>
|
|
35
35
|
deploymentStart?(message: string): void
|
|
36
36
|
deploymentFinish?(succeeded: boolean): void
|
|
37
|
+
activationStart?(message: string): void
|
|
38
|
+
activationFinish?(result?: import('./activation.mjs').ActivationObservation): void
|
|
37
39
|
failedDeployment?(result: { claimUrl: string; workerName: string }): void
|
|
38
40
|
cleanupFailure(message: string): void
|
|
39
41
|
recovery(result: InstallRecovery): void
|
|
@@ -52,6 +54,7 @@ export interface InstallRecovery {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export interface InstallResult {
|
|
57
|
+
activation?: import('./activation.mjs').ActivationObservation
|
|
55
58
|
publicUrl: string
|
|
56
59
|
versionId?: string
|
|
57
60
|
account?: CloudflareAccount
|
|
@@ -130,6 +133,11 @@ export function installEdge(options: {
|
|
|
130
133
|
environment?: NodeJS.ProcessEnv
|
|
131
134
|
createTemporaryDirectory?: () => Promise<string>
|
|
132
135
|
removePath?: typeof import('node:fs/promises').rm
|
|
136
|
+
observeActivation?: (options: {
|
|
137
|
+
publicUrl: string
|
|
138
|
+
mode: RuntimeMode
|
|
139
|
+
signal?: AbortSignal
|
|
140
|
+
}) => Promise<import('./activation.mjs').ActivationObservation>
|
|
133
141
|
signal?: AbortSignal
|
|
134
142
|
}): Promise<InstallResult>
|
|
135
143
|
export function wranglerProcessInvocation(args: string[], options?: {
|
package/scripts/install.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto'
|
|
2
2
|
import { execFile } from 'node:child_process'
|
|
3
|
-
import { chmod, mkdtemp, open,
|
|
3
|
+
import { chmod, mkdtemp, open, rm, writeFile } from 'node:fs/promises'
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { join } from 'node:path'
|
|
@@ -93,6 +93,48 @@ const SENSITIVE_ENV_KEY = /(KEY|PASSWORD|SECRET|TOKEN)/iu
|
|
|
93
93
|
const WORKER_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u
|
|
94
94
|
const CLAIM_URL = /https:\/\/dash\.cloudflare\.com\/claim-preview\?[^\s\u001b]+/u
|
|
95
95
|
const MAX_CAPTURE_BYTES = 2 * 1024 * 1024
|
|
96
|
+
const WINDOWS_ACL_TIMEOUT_MS = 30_000
|
|
97
|
+
const WINDOWS_PRIVATE_DIRECTORY_SCRIPT = String.raw`
|
|
98
|
+
$ErrorActionPreference = 'Stop'
|
|
99
|
+
$directory = [System.IO.Path]::GetFullPath($env:DSH_EDGE_PRIVATE_DIRECTORY)
|
|
100
|
+
if (-not [System.IO.Directory]::Exists($directory)) {
|
|
101
|
+
throw 'Installer temporary directory does not exist.'
|
|
102
|
+
}
|
|
103
|
+
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
104
|
+
$acl = [System.Security.AccessControl.DirectorySecurity]::new()
|
|
105
|
+
$acl.SetOwner($identity)
|
|
106
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
107
|
+
$inheritance = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
108
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
109
|
+
$identity,
|
|
110
|
+
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
111
|
+
$inheritance,
|
|
112
|
+
[System.Security.AccessControl.PropagationFlags]::None,
|
|
113
|
+
[System.Security.AccessControl.AccessControlType]::Allow
|
|
114
|
+
)
|
|
115
|
+
$acl.AddAccessRule($rule)
|
|
116
|
+
[System.IO.Directory]::SetAccessControl($directory, $acl)
|
|
117
|
+
|
|
118
|
+
$verified = [System.IO.Directory]::GetAccessControl(
|
|
119
|
+
$directory,
|
|
120
|
+
[System.Security.AccessControl.AccessControlSections]::Owner -bor [System.Security.AccessControl.AccessControlSections]::Access
|
|
121
|
+
)
|
|
122
|
+
$owner = $verified.GetOwner([System.Security.Principal.SecurityIdentifier])
|
|
123
|
+
$rules = @($verified.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))
|
|
124
|
+
if (-not $verified.AreAccessRulesProtected -or $owner.Value -ne $identity.Value -or $rules.Count -ne 1) {
|
|
125
|
+
throw "Installer temporary directory DACL is not private (protected=$($verified.AreAccessRulesProtected), owner=$($owner.Value), user=$($identity.Value), rules=$($rules.Count))."
|
|
126
|
+
}
|
|
127
|
+
$verifiedRule = $rules[0]
|
|
128
|
+
if (
|
|
129
|
+
$verifiedRule.IdentityReference.Value -ne $identity.Value -or
|
|
130
|
+
$verifiedRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
|
|
131
|
+
$verifiedRule.FileSystemRights -ne [System.Security.AccessControl.FileSystemRights]::FullControl -or
|
|
132
|
+
$verifiedRule.InheritanceFlags -ne $inheritance -or
|
|
133
|
+
$verifiedRule.PropagationFlags -ne [System.Security.AccessControl.PropagationFlags]::None
|
|
134
|
+
) {
|
|
135
|
+
throw 'Installer temporary directory DACL has unexpected access rules.'
|
|
136
|
+
}
|
|
137
|
+
`
|
|
96
138
|
|
|
97
139
|
export class InstallCancelledError extends Error {
|
|
98
140
|
constructor() {
|
|
@@ -285,6 +327,7 @@ export async function installEdge({
|
|
|
285
327
|
environment = process.env,
|
|
286
328
|
createTemporaryDirectory = createPrivateTemporaryDirectory,
|
|
287
329
|
removePath = rm,
|
|
330
|
+
observeActivation,
|
|
288
331
|
signal,
|
|
289
332
|
} = {}) {
|
|
290
333
|
if (ui === undefined) throw new Error('installEdge requires an installer UI.')
|
|
@@ -481,7 +524,7 @@ export async function installEdge({
|
|
|
481
524
|
})
|
|
482
525
|
throw error
|
|
483
526
|
}
|
|
484
|
-
|
|
527
|
+
let result = {
|
|
485
528
|
...deployment,
|
|
486
529
|
account,
|
|
487
530
|
claimUrl,
|
|
@@ -490,6 +533,22 @@ export async function installEdge({
|
|
|
490
533
|
temporary,
|
|
491
534
|
workerName,
|
|
492
535
|
}
|
|
536
|
+
if (observeActivation !== undefined) {
|
|
537
|
+
ui.activationStart?.('Activating the public URL… Cloudflare usually takes 10–30 seconds.')
|
|
538
|
+
try {
|
|
539
|
+
const activation = await observeActivation({
|
|
540
|
+
mode,
|
|
541
|
+
publicUrl: result.publicUrl,
|
|
542
|
+
signal,
|
|
543
|
+
})
|
|
544
|
+
result = { ...result, activation }
|
|
545
|
+
ui.activationFinish?.(activation)
|
|
546
|
+
} catch (error) {
|
|
547
|
+
ui.activationFinish?.()
|
|
548
|
+
ui.recovery(result)
|
|
549
|
+
throw error
|
|
550
|
+
}
|
|
551
|
+
}
|
|
493
552
|
completedResult = result
|
|
494
553
|
} catch (error) {
|
|
495
554
|
primaryError = signal?.aborted ? abortReason(signal, 'Installation interrupted.') : error
|
|
@@ -1001,6 +1060,38 @@ async function readBoundedUtf8File(path, maxBytes) {
|
|
|
1001
1060
|
|
|
1002
1061
|
async function createPrivateTemporaryDirectory() {
|
|
1003
1062
|
const directory = await mkdtemp(join(tmpdir(), 'dsh-edge-install-'))
|
|
1004
|
-
|
|
1005
|
-
|
|
1063
|
+
try {
|
|
1064
|
+
if (process.platform === 'win32') {
|
|
1065
|
+
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR
|
|
1066
|
+
if (systemRoot === undefined || systemRoot === '') {
|
|
1067
|
+
throw new Error('Could not locate Windows PowerShell to secure installer credentials.')
|
|
1068
|
+
}
|
|
1069
|
+
const powershell = join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
|
|
1070
|
+
const encodedCommand = Buffer.from(WINDOWS_PRIVATE_DIRECTORY_SCRIPT, 'utf16le').toString('base64')
|
|
1071
|
+
try {
|
|
1072
|
+
await execa(powershell, [
|
|
1073
|
+
'-NoLogo',
|
|
1074
|
+
'-NoProfile',
|
|
1075
|
+
'-NonInteractive',
|
|
1076
|
+
'-EncodedCommand',
|
|
1077
|
+
encodedCommand,
|
|
1078
|
+
], {
|
|
1079
|
+
env: {
|
|
1080
|
+
...process.env,
|
|
1081
|
+
DSH_EDGE_PRIVATE_DIRECTORY: directory,
|
|
1082
|
+
},
|
|
1083
|
+
timeout: WINDOWS_ACL_TIMEOUT_MS,
|
|
1084
|
+
windowsHide: true,
|
|
1085
|
+
})
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
throw new Error('Could not establish a private Windows ACL for installer credentials.', { cause: error })
|
|
1088
|
+
}
|
|
1089
|
+
} else {
|
|
1090
|
+
await chmod(directory, 0o700)
|
|
1091
|
+
}
|
|
1092
|
+
return directory
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
await rm(directory, { recursive: true, force: true }).catch(() => {})
|
|
1095
|
+
throw error
|
|
1096
|
+
}
|
|
1006
1097
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface BundledComponent {
|
|
2
|
+
author?: string | { name?: string }
|
|
3
|
+
license: string
|
|
4
|
+
name: string
|
|
5
|
+
path: string
|
|
6
|
+
repository?: string | { url?: string }
|
|
7
|
+
version: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LegalDocumentUse {
|
|
11
|
+
file: string
|
|
12
|
+
name: string
|
|
13
|
+
version: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LegalDocument {
|
|
17
|
+
id: string
|
|
18
|
+
text: string
|
|
19
|
+
uses: LegalDocumentUse[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare function collectLicenseDocuments(components: BundledComponent[]): LegalDocument[]
|
|
23
|
+
export declare function renderBundledTerms(components: BundledComponent[]): string
|
package/scripts/legal-files.mjs
CHANGED
|
@@ -1,82 +1,264 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
2
5
|
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { resolvePnpmInvocation } from './pnpm-invocation.mjs'
|
|
3
7
|
|
|
4
|
-
const
|
|
5
|
-
|
|
8
|
+
const MIT_TERMS = `Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.`
|
|
25
|
+
|
|
26
|
+
const edgeLicense = `MIT License
|
|
27
|
+
|
|
28
|
+
Copyright (c) 2026 pawaca
|
|
29
|
+
|
|
30
|
+
${MIT_TERMS}
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
const upstreamLicense = `MIT License
|
|
34
|
+
|
|
35
|
+
Copyright (c) 2026 DeepSeek
|
|
36
|
+
|
|
37
|
+
${MIT_TERMS}`
|
|
38
|
+
|
|
39
|
+
const LEGAL_FILE = /^(?:licen[cs]e|copying|notice)(?:$|[._-])/iu
|
|
40
|
+
const repositoryRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
|
41
|
+
const packageRoot = fileURLToPath(new URL('../', import.meta.url))
|
|
42
|
+
const standaloneRoot = join(packageRoot, 'standalone')
|
|
43
|
+
|
|
44
|
+
function bundledComponents() {
|
|
45
|
+
const invocation = resolvePnpmInvocation(process.env.npm_execpath ?? 'pnpm', [
|
|
46
|
+
'--dir',
|
|
47
|
+
standaloneRoot,
|
|
48
|
+
'licenses',
|
|
49
|
+
'list',
|
|
50
|
+
'--prod',
|
|
51
|
+
'--json',
|
|
52
|
+
])
|
|
53
|
+
const result = spawnSync(invocation.command, invocation.args, { encoding: 'utf8' })
|
|
54
|
+
if (result.error !== undefined) throw result.error
|
|
55
|
+
if (result.status !== 0) {
|
|
56
|
+
throw new Error(`unable to inventory the standalone production closure:\n${result.stdout}${result.stderr}`)
|
|
11
57
|
}
|
|
58
|
+
|
|
59
|
+
const report = JSON.parse(result.stdout)
|
|
60
|
+
const components = new Map()
|
|
61
|
+
for (const [license, packages] of Object.entries(report)) {
|
|
62
|
+
if (!Array.isArray(packages)) throw new Error(`invalid pnpm license group ${license}`)
|
|
63
|
+
for (const item of packages) {
|
|
64
|
+
if (typeof item?.name !== 'string' || !Array.isArray(item.versions) || !Array.isArray(item.paths)) {
|
|
65
|
+
throw new Error(`invalid pnpm license entry in ${license}`)
|
|
66
|
+
}
|
|
67
|
+
for (const path of item.paths) {
|
|
68
|
+
if (typeof path !== 'string') throw new Error(`invalid package path for ${item.name}`)
|
|
69
|
+
const manifest = JSON.parse(readFileSync(join(path, 'package.json'), 'utf8'))
|
|
70
|
+
if (manifest.os !== undefined || manifest.cpu !== undefined || manifest.libc !== undefined) continue
|
|
71
|
+
if (typeof manifest.version !== 'string' || !item.versions.includes(manifest.version)) {
|
|
72
|
+
throw new Error(`pnpm license versions do not cover ${item.name} at ${path}`)
|
|
73
|
+
}
|
|
74
|
+
const component = {
|
|
75
|
+
license,
|
|
76
|
+
name: item.name,
|
|
77
|
+
version: manifest.version,
|
|
78
|
+
path,
|
|
79
|
+
author: manifest.author,
|
|
80
|
+
repository: manifest.repository,
|
|
81
|
+
}
|
|
82
|
+
const key = `${component.name}\0${component.version}\0${component.license}`
|
|
83
|
+
const current = components.get(key)
|
|
84
|
+
if (current === undefined || component.path.localeCompare(current.path, 'en') < 0) {
|
|
85
|
+
components.set(key, component)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return [...components.values()].sort((left, right) => (
|
|
91
|
+
left.name.localeCompare(right.name, 'en')
|
|
92
|
+
|| left.version.localeCompare(right.version, 'en')
|
|
93
|
+
|| left.license.localeCompare(right.license, 'en')
|
|
94
|
+
))
|
|
12
95
|
}
|
|
13
96
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
97
|
+
function authorName(author) {
|
|
98
|
+
if (typeof author === 'string' && author.trim() !== '') return author.trim()
|
|
99
|
+
if (author !== null && typeof author === 'object' && typeof author.name === 'string' && author.name.trim() !== '') {
|
|
100
|
+
return author.name.trim()
|
|
18
101
|
}
|
|
19
|
-
return upstream.replace(upstreamCopyright, edgeCopyright)
|
|
20
102
|
}
|
|
21
103
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (inventoryStart < 0) {
|
|
27
|
-
throw new Error('repository-root THIRD_PARTY_NOTICES.md has no dependency inventory')
|
|
104
|
+
function repositoryUrl(repository) {
|
|
105
|
+
if (typeof repository === 'string' && repository.trim() !== '') return repository.trim()
|
|
106
|
+
if (repository !== null && typeof repository === 'object' && typeof repository.url === 'string' && repository.url.trim() !== '') {
|
|
107
|
+
return repository.url.trim()
|
|
28
108
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeLegalText(text) {
|
|
112
|
+
return text
|
|
113
|
+
.replace(/^\uFEFF/u, '')
|
|
114
|
+
.replaceAll('\r\n', '\n')
|
|
115
|
+
.split('\n')
|
|
116
|
+
.map(line => line.trimEnd())
|
|
117
|
+
.join('\n')
|
|
32
118
|
.trim()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function missingLicenseFallback(component, apacheTerms) {
|
|
122
|
+
const author = authorName(component.author) ?? `contributors to ${component.name}`
|
|
123
|
+
const repository = repositoryUrl(component.repository)
|
|
124
|
+
const provenance = [
|
|
125
|
+
`The published ${component.name}@${component.version} package declared ${component.license}`,
|
|
126
|
+
'but did not include a license or notice file.',
|
|
127
|
+
`Attribution from its package metadata: ${author}.`,
|
|
128
|
+
...(repository === undefined ? [] : [`Repository: ${repository}.`]),
|
|
129
|
+
].join(' ')
|
|
130
|
+
if (component.license === 'MIT' || component.license === 'MIT OR Apache-2.0') {
|
|
131
|
+
return `${provenance}\n\nThe MIT branch is reproduced for this distribution:\n\nMIT License\n\nPublished package author: ${author}\n\n${MIT_TERMS}`
|
|
132
|
+
}
|
|
133
|
+
if (component.license === 'Apache-2.0' && apacheTerms !== undefined) {
|
|
134
|
+
return `${provenance}\n\n${apacheTerms}`
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`${component.name}@${component.version} has no distributable license text for ${component.license}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function collectLicenseDocuments(components) {
|
|
140
|
+
const documents = new Map()
|
|
141
|
+
const componentsWithoutFiles = []
|
|
142
|
+
let apacheTerms
|
|
143
|
+
const addDocument = (text, use) => {
|
|
144
|
+
const normalized = normalizeLegalText(text)
|
|
145
|
+
if (normalized === '') throw new Error(`empty legal text for ${use.name}@${use.version}`)
|
|
146
|
+
const document = documents.get(normalized) ?? { text: normalized, uses: [] }
|
|
147
|
+
document.uses.push(use)
|
|
148
|
+
documents.set(normalized, document)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const component of components) {
|
|
152
|
+
const files = readdirSync(component.path)
|
|
153
|
+
.filter(name => LEGAL_FILE.test(name))
|
|
154
|
+
.sort((left, right) => left.localeCompare(right, 'en'))
|
|
155
|
+
if (files.length === 0) {
|
|
156
|
+
componentsWithoutFiles.push(component)
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
for (const file of files) {
|
|
160
|
+
const text = normalizeLegalText(readFileSync(join(component.path, file), 'utf8'))
|
|
161
|
+
if (component.license === 'Apache-2.0' && /^Apache License\s+Version 2\.0/imu.test(text)) {
|
|
162
|
+
apacheTerms ??= text
|
|
163
|
+
}
|
|
164
|
+
addDocument(text, { file, name: component.name, version: component.version })
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const component of componentsWithoutFiles) {
|
|
169
|
+
addDocument(missingLicenseFallback(component, apacheTerms), {
|
|
170
|
+
file: 'package metadata fallback',
|
|
171
|
+
name: component.name,
|
|
172
|
+
version: component.version,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
33
175
|
|
|
176
|
+
return [...documents.values()]
|
|
177
|
+
.map(document => ({
|
|
178
|
+
...document,
|
|
179
|
+
id: createHash('sha256').update(document.text).digest('hex').slice(0, 12),
|
|
180
|
+
uses: document.uses.sort((left, right) => (
|
|
181
|
+
left.name.localeCompare(right.name, 'en')
|
|
182
|
+
|| left.version.localeCompare(right.version, 'en')
|
|
183
|
+
|| left.file.localeCompare(right.file, 'en')
|
|
184
|
+
)),
|
|
185
|
+
}))
|
|
186
|
+
.sort((left, right) => left.id.localeCompare(right.id, 'en'))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function renderBundledTerms(components) {
|
|
190
|
+
return collectLicenseDocuments(components)
|
|
191
|
+
.map(document => {
|
|
192
|
+
const uses = document.uses
|
|
193
|
+
.map(use => `- \`${use.name}@${use.version}\` (\`${use.file}\`)`)
|
|
194
|
+
.join('\n')
|
|
195
|
+
return `### License/notice ${document.id}\n\nApplies to:\n\n${uses}\n\n\`\`\`text\n${document.text}\n\`\`\``
|
|
196
|
+
})
|
|
197
|
+
.join('\n\n')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderNotices(components) {
|
|
201
|
+
const inventory = components
|
|
202
|
+
.map(component => `| \`${component.name}@${component.version}\` | \`${component.license}\` |`)
|
|
203
|
+
.join('\n')
|
|
204
|
+
const bundledTerms = renderBundledTerms(components)
|
|
34
205
|
return `<!-- Generated by apps/dsh-edge/scripts/legal-files.mjs — do not edit by hand.
|
|
35
206
|
Run \`pnpm --filter dsh-edge run legal:write\` to regenerate. -->
|
|
36
207
|
|
|
37
208
|
# Third-Party Notices
|
|
38
209
|
|
|
39
|
-
\`dsh-edge\` is an independent community project
|
|
210
|
+
\`dsh-edge\` is an independent community project maintained by pawaca. It is not affiliated with or endorsed by DeepSeek.
|
|
40
211
|
|
|
41
212
|
## DeepSeek Harness
|
|
42
213
|
|
|
43
|
-
\`dsh-edge\`
|
|
214
|
+
\`dsh-edge\` assembles published DeepSeek Harness packages and applies six version-bound adaptations to the pinned \`0.1.0-rc.7\` release. DeepSeek Harness remains under its upstream MIT license:
|
|
44
215
|
|
|
45
216
|
\`\`\`text
|
|
46
217
|
${upstreamLicense}
|
|
47
218
|
\`\`\`
|
|
48
219
|
|
|
49
|
-
##
|
|
220
|
+
## Bundled component inventory
|
|
221
|
+
|
|
222
|
+
The following ${components.length} package versions form the conservative, platform-neutral production closure used to assemble the Web and Worker artifacts in this distribution. Platform-specific build binaries that are not distributed in those artifacts are excluded; tree shaking can omit additional code. The inventory and legal texts are generated during packaging rather than referring recipients to a source checkout. License expressions are SPDX identifiers supplied by each package. Dependencies installed separately by npm remain declared in this package's \`package.json\`.
|
|
50
223
|
|
|
51
|
-
|
|
224
|
+
| Component | License |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
${inventory}
|
|
52
227
|
|
|
53
|
-
|
|
228
|
+
## Bundled license and notice texts
|
|
229
|
+
|
|
230
|
+
License, copyright, attribution, and NOTICE files supplied by the published packages are reproduced below with line endings and trailing whitespace normalized and identical texts deduplicated. When a package omits those files, the entry records that fact, preserves its published metadata attribution, and reproduces a permitted branch of its declared SPDX terms.
|
|
231
|
+
|
|
232
|
+
${bundledTerms}
|
|
54
233
|
`
|
|
55
234
|
}
|
|
56
235
|
|
|
57
|
-
function
|
|
236
|
+
function legalFiles() {
|
|
237
|
+
const notices = renderNotices(bundledComponents())
|
|
58
238
|
return new Map([
|
|
59
|
-
['LICENSE',
|
|
60
|
-
['THIRD_PARTY_NOTICES.md',
|
|
239
|
+
[join(repositoryRoot, 'LICENSE'), edgeLicense],
|
|
240
|
+
[join(repositoryRoot, 'THIRD_PARTY_NOTICES.md'), notices],
|
|
241
|
+
[join(packageRoot, 'LICENSE'), edgeLicense],
|
|
242
|
+
[join(packageRoot, 'THIRD_PARTY_NOTICES.md'), notices],
|
|
61
243
|
])
|
|
62
244
|
}
|
|
63
245
|
|
|
64
246
|
function verify() {
|
|
65
|
-
for (const [
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
throw new Error(`${name} is stale; run \`pnpm --filter dsh-edge run legal:write\``)
|
|
247
|
+
for (const [path, expected] of legalFiles()) {
|
|
248
|
+
if (readFileSync(path, 'utf8') !== expected) {
|
|
249
|
+
throw new Error(`${path} is stale; run \`pnpm --filter dsh-edge run legal:write\``)
|
|
69
250
|
}
|
|
70
251
|
}
|
|
71
252
|
}
|
|
72
253
|
|
|
73
254
|
function write() {
|
|
74
|
-
for (const [
|
|
75
|
-
writeFileSync(paths(name).target, content)
|
|
76
|
-
}
|
|
255
|
+
for (const [path, content] of legalFiles()) writeFileSync(path, content)
|
|
77
256
|
}
|
|
78
257
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
258
|
+
if (process.argv[1] !== undefined
|
|
259
|
+
&& realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
260
|
+
const action = process.argv[2]
|
|
261
|
+
if (action === 'verify') verify()
|
|
262
|
+
else if (action === 'write') write()
|
|
263
|
+
else throw new Error('Usage: node scripts/legal-files.mjs <verify|write>')
|
|
264
|
+
}
|