dsh-tui-theme 0.3.2 → 0.4.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.
@@ -84,11 +84,11 @@ function sectionDefinition(cordis) {
84
84
  fields: [
85
85
  {
86
86
  path: ['followSystem'],
87
- label: 'Follow terminal background',
88
- descriptions: { zh: '跟随终端背景' },
89
- hint: 'Auto-switch between Pink Day and Pink Night by terminal/system background. First enabling or a background flip applies from the next boot.',
87
+ label: 'Use cached terminal background',
88
+ descriptions: { zh: '使用已缓存的终端背景' },
89
+ hint: 'Apply a previously saved terminal background result at startup. dsh-TUI 0.9.2 does not expose a safe plugin query, so this plugin does not refresh the cache.',
90
90
  hintDescriptions: {
91
- zh: '按终端/系统背景色在昼樱与夜樱间自动切换。首次开启或背景刚翻转后,自下次启动生效。',
91
+ zh: '启动时应用此前保存的终端背景结果。dsh-TUI 0.9.2 未提供安全的插件查询接缝,因此本插件不会刷新该缓存。',
92
92
  },
93
93
  kind: 'boolean',
94
94
  format: (value) => String(value ?? cordis.followSystem),
@@ -92,14 +92,14 @@ export function startStatusLine(ctx, getEffective) {
92
92
  // Display garnish only: a rendering hiccup must never travel upward.
93
93
  }
94
94
  };
95
- ctx.on('session/event', (session, event) => {
95
+ statusCtx.on('session/event', (session, event) => {
96
96
  current = session;
97
97
  if (event?.type === 'turn/end') {
98
98
  turns.set(session, (turns.get(session) ?? 0) + 1);
99
99
  }
100
100
  render();
101
101
  });
102
- ctx.on('session/disposed', session => {
102
+ statusCtx.on('session/disposed', session => {
103
103
  turns.delete(session);
104
104
  if (current === session)
105
105
  current = undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"themeAssets.d.ts","sourceRoot":"","sources":["../../src/themeAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC;AAID,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,2DAA2D;AAC3D,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,kBAAkB,CA2BpB"}
1
+ {"version":3,"file":"themeAssets.d.ts","sourceRoot":"","sources":["../../src/themeAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC;AAID,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,2DAA2D;AAC3D,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,kBAAkB,CA+BpB"}
@@ -7,7 +7,7 @@
7
7
  * theme garnish must never break the TUI's boot.
8
8
  * @module dsh-tui-pink-theme/themeAssets
9
9
  */
10
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
10
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
11
11
  import { homedir } from 'node:os';
12
12
  import { dirname, join } from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
@@ -42,17 +42,23 @@ export function installBundledThemes(targetDir = themesTargetDir(), sourceDir =
42
42
  return { installed, skipped, failed: [sourceDir] };
43
43
  }
44
44
  for (const file of files) {
45
+ const target = join(targetDir, file);
45
46
  try {
46
- const target = join(targetDir, file);
47
- if (existsSync(target)) {
48
- skipped.push(file);
49
- continue;
50
- }
51
47
  const text = readFileSync(join(sourceDir, file), 'utf8');
52
48
  JSON.parse(text); // our own asset, but never write a corrupt file out
53
49
  mkdirSync(targetDir, { recursive: true });
54
- writeFileSync(target, text);
55
- installed.push(file);
50
+ try {
51
+ writeFileSync(target, text, { flag: 'wx' });
52
+ installed.push(file);
53
+ }
54
+ catch (error) {
55
+ if (error.code === 'EEXIST') {
56
+ skipped.push(file);
57
+ }
58
+ else {
59
+ failed.push(file);
60
+ }
61
+ }
56
62
  }
57
63
  catch {
58
64
  failed.push(file);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-tui-theme",
3
- "version": "0.3.2",
4
- "description": "Sakura-pink themes for dsh-TUI with terminal-background auto-follow (pink-day/pink-night), a blossom status line, and a /settings section. No shortcuts, no commands.",
3
+ "version": "0.4.0",
4
+ "description": "Sakura-pink themes for dsh-TUI with optional cached background follow (pink-day/pink-night), a blossom status line, and a /settings section. No shortcuts, no commands.",
5
5
  "type": "module",
6
6
  "main": "lib/types/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -17,6 +17,7 @@
17
17
  "lib",
18
18
  "themes",
19
19
  "docs",
20
+ "scripts",
20
21
  "cordis.patch.yml"
21
22
  ],
22
23
  "engines": {
@@ -24,8 +25,11 @@
24
25
  },
25
26
  "scripts": {
26
27
  "build": "tsc -p tsconfig.json",
27
- "preverify": "npm run build",
28
- "verify": "node scripts/verify.mjs"
28
+ "verify": "node scripts/verify.mjs",
29
+ "verify:package": "node scripts/verify-package.mjs",
30
+ "verify:host": "node scripts/headless-order-test.mjs && node --import tsx/esm scripts/validate-themes-against-host.mjs",
31
+ "release:check": "npm run build && npm run verify && npm run verify:package",
32
+ "prepack": "npm run release:check"
29
33
  },
30
34
  "license": "MIT",
31
35
  "author": "xiaoxiaohaigui",
@@ -51,19 +55,19 @@
51
55
  "patch": "./cordis.patch.yml"
52
56
  }
53
57
  },
54
- "dependencies": {
55
- "@deepseek-ai/schemastery": "^3.18.1"
56
- },
57
58
  "peerDependencies": {
58
59
  "@deepseek-ai/cordis": "^4.0.1",
59
60
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1",
60
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1"
61
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1",
62
+ "@deepseek-ai/schemastery": "^3.18.1"
61
63
  },
62
64
  "devDependencies": {
63
65
  "@deepseek-ai/cordis": "^4.0.1",
64
66
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1",
65
67
  "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1",
68
+ "@deepseek-ai/schemastery": "^3.18.1",
66
69
  "@types/node": "^22.0.0",
70
+ "tsx": "^4.23.12",
67
71
  "typescript": "^6.0.3"
68
72
  }
69
73
  }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * End-to-end order test with the installed dsh-TUI services. Composes this
3
+ * plugin before the extension services to exercise late service injection.
4
+ *
5
+ * DSH_TUI_ADAPTER_DIR must point at dsh-TUI's lib/types/dsh-adapter directory.
6
+ */
7
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+ import { fileURLToPath, pathToFileURL } from 'node:url'
11
+ import { createRequire } from 'node:module'
12
+ import assert from 'node:assert/strict'
13
+
14
+ const pluginRoot = fileURLToPath(new URL('..', import.meta.url))
15
+ const adapter = process.env.DSH_TUI_ADAPTER_DIR
16
+ if (adapter === undefined || adapter === '') {
17
+ throw new Error('DSH_TUI_ADAPTER_DIR must point at dsh-TUI lib/types/dsh-adapter for this host integration test.')
18
+ }
19
+
20
+ if (!existsSync(join(adapter, 'extensions.js'))) {
21
+ throw new Error(`dsh-TUI adapter not found at ${adapter}`)
22
+ }
23
+
24
+ const hostPackagePath = join(adapter, '..', '..', '..', 'package.json')
25
+ if (!existsSync(hostPackagePath)) {
26
+ throw new Error(`dsh-TUI package metadata not found above adapter at ${adapter}`)
27
+ }
28
+ const hostPackage = JSON.parse(readFileSync(hostPackagePath, 'utf8'))
29
+ assert.equal(hostPackage.version, '0.9.2', `expected dsh-TUI adapter 0.9.2, received ${hostPackage.version}`)
30
+
31
+ const sandbox = mkdtempSync(join(tmpdir(), 'pink-order-'))
32
+ process.env.USERPROFILE = sandbox
33
+ process.env.HOME = sandbox
34
+
35
+ const dataDir = join(sandbox, '.dsh-tui')
36
+ mkdirSync(dataDir, { recursive: true })
37
+ writeFileSync(join(dataDir, 'theme.json'), JSON.stringify({ theme: 'pink-night' }, null, 2))
38
+
39
+ const req = createRequire(join(adapter, 'extensions.js'))
40
+ const { Context } = await import(pathToFileURL(req.resolve('@deepseek-ai/cordis')).href)
41
+ const extensions = await import(pathToFileURL(join(adapter, 'extensions.js')).href)
42
+ const ledgerModule = await import(pathToFileURL(join(adapter, 'effect-ledger.js')).href)
43
+ const statusModule = await import(pathToFileURL(join(adapter, 'status.js')).href)
44
+ const pink = await import(pathToFileURL(join(pluginRoot, 'lib', 'types', 'index.js')).href)
45
+
46
+ const app = new Context()
47
+ await app.plugin(ledgerModule.default)
48
+ await app.plugin(pink)
49
+ await app.plugin(extensions.default ?? extensions)
50
+ await new Promise(resolve => setTimeout(resolve, 300))
51
+
52
+ const ledger = join(dataDir, 'effect-ledger.jsonl')
53
+ const pinkBinds = existsSync(ledger)
54
+ ? readFileSync(ledger, 'utf8').trim().split('\n')
55
+ .filter(Boolean)
56
+ .map(line => JSON.parse(line))
57
+ .filter(entry => entry.resource?.id === 'dsh-tui-theme')
58
+ : []
59
+ const runtime = app.get('tuiStatus')
60
+ const snapshot = statusModule.getHostStatusStore(runtime)?.getSnapshot()
61
+
62
+ assert.ok(pinkBinds.length > 0, 'plugin must bind through the late status service')
63
+ assert.ok(
64
+ snapshot?.some?.(entry => entry.key === 'dsh-tui-theme'),
65
+ 'status store must contain the plugin contribution',
66
+ )
67
+ console.log(`✓ headless order: ${pinkBinds.length} ledger bind(s), visible status contribution`)
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Validate bundled themes against the real dsh-TUI source implementation.
3
+ * DSH_TUI_SOURCE_ROOT must point at a dsh-TUI source checkout.
4
+ *
5
+ * Run with: npx -y tsx scripts/validate-themes-against-host.mjs
6
+ */
7
+ import { existsSync, mkdtempSync, readFileSync, readdirSync } from 'node:fs'
8
+ import { tmpdir } from 'node:os'
9
+ import { join, resolve } from 'node:path'
10
+ import { fileURLToPath, pathToFileURL } from 'node:url'
11
+ import assert from 'node:assert/strict'
12
+
13
+ const pluginRoot = fileURLToPath(new URL('..', import.meta.url))
14
+ const sourceRoot = process.env.DSH_TUI_SOURCE_ROOT
15
+ if (sourceRoot === undefined || sourceRoot === '') {
16
+ throw new Error('DSH_TUI_SOURCE_ROOT must point at a dsh-TUI source checkout for this host theme validation.')
17
+ }
18
+ const hostRoot = resolve(sourceRoot)
19
+ const customThemePath = join(hostRoot, 'src', 'customTheme.ts')
20
+ const themePath = join(hostRoot, 'src', 'theme.ts')
21
+
22
+ if (!existsSync(customThemePath) || !existsSync(themePath)) {
23
+ throw new Error(`dsh-TUI sources not found at ${hostRoot}`)
24
+ }
25
+
26
+ const hostPackagePath = join(hostRoot, 'package.json')
27
+ if (!existsSync(hostPackagePath)) {
28
+ throw new Error(`dsh-TUI package metadata not found at ${hostPackagePath}`)
29
+ }
30
+ const hostPackage = JSON.parse(readFileSync(hostPackagePath, 'utf8'))
31
+ assert.equal(hostPackage.version, '0.9.2', `expected dsh-TUI source 0.9.2, received ${hostPackage.version}`)
32
+
33
+ const sandboxHome = mkdtempSync(join(tmpdir(), 'pink-theme-host-validate-'))
34
+ process.env.USERPROFILE = sandboxHome
35
+ process.env.HOME = sandboxHome
36
+
37
+ const themesDir = join(pluginRoot, 'themes')
38
+ const { parseCustomTheme, buildTheme } = await import(pathToFileURL(customThemePath).href)
39
+ const { getTheme, isLightThemeActive, registerCustomThemeResolver } = await import(
40
+ pathToFileURL(themePath).href
41
+ )
42
+
43
+ const allKeys = Object.keys(getTheme('dark'))
44
+ assert.ok(allKeys.length >= 90, 'host Theme key count drifted; re-check coverage')
45
+
46
+ function parseColor(value) {
47
+ if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
48
+ const n = Number.parseInt(value.slice(1), 16)
49
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
50
+ }
51
+ const match = /^rgb\((\d+),(\d+),(\d+)\)$/.exec(value)
52
+ return match === null ? undefined : [Number(match[1]), Number(match[2]), Number(match[3])]
53
+ }
54
+
55
+ function luminance(color) {
56
+ const channel = value => {
57
+ const normalized = value / 255
58
+ return normalized <= 0.03928
59
+ ? normalized / 12.92
60
+ : ((normalized + 0.055) / 1.055) ** 2.4
61
+ }
62
+ return 0.2126 * channel(color[0]) + 0.7152 * channel(color[1]) + 0.0722 * channel(color[2])
63
+ }
64
+
65
+ function contrast(foreground, background) {
66
+ const [high, low] = [luminance(foreground), luminance(background)].sort((a, b) => b - a)
67
+ return (high + 0.05) / (low + 0.05)
68
+ }
69
+
70
+ const expectedLight = { 'pink-night': false, 'pink-day': true, 'pink-ansi': false }
71
+ const built = {}
72
+ registerCustomThemeResolver(name => built[name])
73
+
74
+ for (const file of readdirSync(themesDir).filter(name => name.endsWith('.json'))) {
75
+ const warnings = []
76
+ const originalWarn = console.warn
77
+ console.warn = message => warnings.push(String(message))
78
+ let spec
79
+ try {
80
+ spec = parseCustomTheme(readFileSync(join(themesDir, file), 'utf8'), file)
81
+ } finally {
82
+ console.warn = originalWarn
83
+ }
84
+
85
+ assert.notEqual(spec, undefined, `${file} must parse`)
86
+ assert.deepEqual(warnings, [], `${file} must produce zero host warnings`)
87
+ const missing = allKeys.filter(key => key !== 'userMessageBackground' && !(key in spec.colors))
88
+ assert.deepEqual(missing, [], `${file} must cover every Theme key`)
89
+
90
+ const theme = buildTheme(spec)
91
+ built[spec.name] = theme
92
+ assert.equal(isLightThemeActive(spec.name), expectedLight[spec.name], `${spec.name} identity`)
93
+ }
94
+
95
+ for (const [name, light] of Object.entries(expectedLight)) {
96
+ assert.equal(isLightThemeActive(name), light, `${name} identity via resolver`)
97
+ }
98
+
99
+ const cases = [
100
+ ['pink-night', 'text', '#1E1E1E', 4.5],
101
+ ['pink-night', 'claude', '#1E1E1E', 3.0],
102
+ ['pink-night', 'inactive', '#1E1E1E', 2.5],
103
+ ['pink-day', 'text', '#F6F3ED', 4.5],
104
+ ['pink-day', 'claude', '#F6F3ED', 3.0],
105
+ ['pink-day', 'inactive', '#F6F3ED', 2.5],
106
+ ]
107
+ for (const [theme, key, background, minimum] of cases) {
108
+ const foregroundRgb = parseColor(built[theme][key])
109
+ const backgroundRgb = parseColor(background)
110
+ assert.notEqual(foregroundRgb, undefined, `${theme}.${key} must be parseable`)
111
+ const ratio = contrast(foregroundRgb, backgroundRgb)
112
+ assert.ok(ratio >= minimum, `${theme}.${key} contrast ${ratio.toFixed(2)} >= ${minimum}`)
113
+ }
114
+
115
+ console.log(`✓ host theme validation: ${Object.keys(built).length} themes, ${allKeys.length - 1} keys each`)
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Release artifact contract: inspect npm's dry-run manifest without creating
3
+ * a tarball, then ensure consumers receive every advertised runtime asset.
4
+ */
5
+ import { execFileSync } from 'node:child_process'
6
+ import { existsSync, readFileSync } from 'node:fs'
7
+ import { fileURLToPath } from 'node:url'
8
+ import assert from 'node:assert/strict'
9
+
10
+ const pluginRoot = fileURLToPath(new URL('..', import.meta.url))
11
+ const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
12
+ const lockfile = JSON.parse(readFileSync(new URL('../package-lock.json', import.meta.url), 'utf8'))
13
+ const packCommand = process.platform === 'win32'
14
+ ? { file: 'cmd.exe', args: ['/d', '/s', '/c', 'npm pack --dry-run --json --ignore-scripts'] }
15
+ : { file: 'npm', args: ['pack', '--dry-run', '--json', '--ignore-scripts'] }
16
+ const manifest = JSON.parse(execFileSync(packCommand.file, packCommand.args, {
17
+ cwd: pluginRoot,
18
+ encoding: 'utf8',
19
+ }))
20
+ const packed = manifest[0]
21
+ assert.ok(packed, 'npm pack must report one package')
22
+ assert.equal(packed.name, 'dsh-tui-theme')
23
+ assert.equal(packed.version, packageJson.version)
24
+
25
+ const files = new Set(packed.files.map(entry => entry.path))
26
+ for (const required of [
27
+ 'package.json',
28
+ 'README.md',
29
+ 'LICENSE',
30
+ 'cordis.patch.yml',
31
+ 'lib/types/index.js',
32
+ 'lib/types/index.d.ts',
33
+ 'lib/types/autoTheme.js',
34
+ 'lib/types/autoTheme.d.ts',
35
+ 'lib/types/settingsSection.js',
36
+ 'lib/types/settingsSection.d.ts',
37
+ 'lib/types/statusLine.js',
38
+ 'lib/types/statusLine.d.ts',
39
+ 'lib/types/themeAssets.js',
40
+ 'lib/types/themeAssets.d.ts',
41
+ 'themes/pink-night.json',
42
+ 'themes/pink-day.json',
43
+ 'themes/pink-ansi.json',
44
+ 'docs/screenshots/pink-day.png',
45
+ 'docs/screenshots/pink-night.png',
46
+ 'docs/screenshots/settings.png',
47
+ 'scripts/verify.mjs',
48
+ 'scripts/verify-package.mjs',
49
+ 'scripts/headless-order-test.mjs',
50
+ 'scripts/validate-themes-against-host.mjs',
51
+ ]) {
52
+ assert.ok(files.has(required), `published package must include ${required}`)
53
+ }
54
+
55
+ const exportedPaths = [packageJson.main, packageJson.types, packageJson.dsh.bundle.patch]
56
+ for (const entry of Object.values(packageJson.exports)) {
57
+ if (typeof entry === 'string') exportedPaths.push(entry)
58
+ else if (entry !== null && typeof entry === 'object') exportedPaths.push(...Object.values(entry))
59
+ }
60
+ for (const entry of exportedPaths) {
61
+ assert.equal(typeof entry, 'string')
62
+ assert.ok(files.has(entry.replace(/^\.\//, '')), `published package must include declared entry ${entry}`)
63
+ }
64
+
65
+ for (const name of Object.keys(packageJson.peerDependencies)) {
66
+ if (!name.startsWith('@deepseek-ai/')) continue
67
+ assert.equal(packageJson.dependencies?.[name], undefined, `${name} must not be a runtime dependency`)
68
+ assert.equal(packageJson.devDependencies?.[name], packageJson.peerDependencies[name], `${name} peer and dev ranges must match`)
69
+ }
70
+ assert.equal(packageJson.dependencies?.['@deepseek-ai/schemastery'], undefined)
71
+ const rootLock = lockfile.packages?.['']
72
+ assert.ok(rootLock, 'lockfile must have root metadata')
73
+ assert.equal(lockfile.version, packageJson.version)
74
+ assert.deepEqual(rootLock.devDependencies, packageJson.devDependencies)
75
+ assert.deepEqual(rootLock.peerDependencies, packageJson.peerDependencies)
76
+ assert.equal(rootLock.dependencies, undefined)
77
+ assert.equal(existsSync(new URL('../lib/types/index.js', import.meta.url)), true)
78
+
79
+ console.log(`✓ package manifest: ${packed.name}@${packed.version}, ${files.size} files`)