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.
Files changed (49) hide show
  1. package/README.i18n.yaml +6 -6
  2. package/README.md +25 -10
  3. package/README.zh.md +25 -10
  4. package/THIRD_PARTY_NOTICES.md +10019 -187
  5. package/dist/index.html +1 -1
  6. package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +7 -7
  7. package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +61 -61
  8. package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +14 -14
  9. package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +287 -287
  10. package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +10 -10
  11. package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +11 -11
  12. package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +15 -15
  13. package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +8 -8
  14. package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +27 -27
  15. package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +8 -8
  16. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +31 -31
  17. package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +23 -23
  18. package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +19 -19
  19. package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +29 -29
  20. package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +7 -7
  21. package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +64 -64
  22. package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +183 -183
  23. package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +44 -44
  24. package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +24 -24
  25. package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +79 -79
  26. package/dist/plugins/dsh-edge-client-ui/client.js +68 -53
  27. package/package.json +22 -22
  28. package/scripts/activation.d.mts +34 -0
  29. package/scripts/activation.mjs +133 -0
  30. package/scripts/cli.d.mts +9 -0
  31. package/scripts/cli.mjs +66 -3
  32. package/scripts/dev.mjs +38 -0
  33. package/scripts/install.d.mts +8 -0
  34. package/scripts/install.mjs +95 -4
  35. package/scripts/legal-files.d.mts +23 -0
  36. package/scripts/legal-files.mjs +222 -40
  37. package/scripts/pnpm-invocation.d.mts +14 -0
  38. package/scripts/pnpm-invocation.mjs +25 -0
  39. package/scripts/publish.d.mts +25 -0
  40. package/scripts/publish.mjs +145 -0
  41. package/scripts/smoke-installed.mjs +27 -22
  42. package/scripts/verify-packed.mjs +8 -0
  43. package/scripts/wrangler-config-core.mjs +107 -0
  44. package/scripts/wrangler-config.d.mts +2 -0
  45. package/scripts/wrangler-config.mjs +9 -78
  46. package/worker/direct/index.js +1136 -1139
  47. package/worker/isolated/index.js +872 -875
  48. package/scripts/assemble-web.mjs +0 -230
  49. 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: message => log(clack.intro, message),
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
- note(lines.join('\n'), command === 'upgrade' ? 'dsh-edge upgraded' : 'dsh-edge installed')
217
- log(clack.outro, command === 'upgrade' ? 'Upgrade handoff complete.' : 'Installation handoff complete.')
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),
@@ -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
+ }
@@ -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?: {
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto'
2
2
  import { execFile } from 'node:child_process'
3
- import { chmod, mkdtemp, open, readFile, rm, writeFile } from 'node:fs/promises'
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
- const result = {
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
- await chmod(directory, 0o700)
1005
- return directory
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
@@ -1,82 +1,264 @@
1
- import { readFileSync, writeFileSync } from 'node:fs'
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 upstreamCopyright = 'Copyright (c) 2026 DeepSeek'
5
- const edgeCopyright = 'Copyright (c) 2026 pawaca'
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
- function paths(name) {
8
- return {
9
- source: fileURLToPath(new URL(`../../../${name}`, import.meta.url)),
10
- target: fileURLToPath(new URL(`../${name}`, import.meta.url)),
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 renderLicense() {
15
- const upstream = readFileSync(paths('LICENSE').source, 'utf8')
16
- if (!upstream.includes(upstreamCopyright)) {
17
- throw new Error('repository-root LICENSE no longer contains the expected DeepSeek copyright')
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 renderNotices() {
23
- const upstreamLicense = readFileSync(paths('LICENSE').source, 'utf8').trim()
24
- const repositoryNotices = readFileSync(paths('THIRD_PARTY_NOTICES.md').source, 'utf8')
25
- const inventoryStart = repositoryNotices.indexOf('## Vendored source')
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
- const upstreamInventory = repositoryNotices
30
- .slice(inventoryStart)
31
- .replace(/^## /gmu, '### ')
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 and is not affiliated with or endorsed by DeepSeek.
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\` includes and adapts DeepSeek Harness. DeepSeek Harness retains its upstream copyright and MIT license:
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
- ## DeepSeek Harness dependency notices
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
- The following inventory is reproduced from the DeepSeek Harness source distribution. Each project remains under its own license; nothing in this file changes those terms.
224
+ | Component | License |
225
+ | --- | --- |
226
+ ${inventory}
52
227
 
53
- ${upstreamInventory}
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 expectedFiles() {
236
+ function legalFiles() {
237
+ const notices = renderNotices(bundledComponents())
58
238
  return new Map([
59
- ['LICENSE', renderLicense()],
60
- ['THIRD_PARTY_NOTICES.md', renderNotices()],
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 [name, expected] of expectedFiles()) {
66
- const { target } = paths(name)
67
- if (readFileSync(target, 'utf8') !== expected) {
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 [name, content] of expectedFiles()) {
75
- writeFileSync(paths(name).target, content)
76
- }
255
+ for (const [path, content] of legalFiles()) writeFileSync(path, content)
77
256
  }
78
257
 
79
- const action = process.argv[2]
80
- if (action === 'verify') verify()
81
- else if (action === 'write') write()
82
- else throw new Error('Usage: node scripts/legal-files.mjs <verify|write>')
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
+ }