dsh-tui-theme 0.3.1 → 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.
- package/README.md +146 -120
- package/cordis.patch.yml +16 -16
- package/lib/types/autoTheme.d.ts +16 -40
- package/lib/types/autoTheme.d.ts.map +1 -1
- package/lib/types/autoTheme.js +40 -127
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +13 -23
- package/lib/types/settingsSection.d.ts +1 -1
- package/lib/types/settingsSection.d.ts.map +1 -1
- package/lib/types/settingsSection.js +34 -27
- package/lib/types/statusLine.d.ts.map +1 -1
- package/lib/types/statusLine.js +6 -2
- package/lib/types/themeAssets.d.ts.map +1 -1
- package/lib/types/themeAssets.js +14 -8
- package/package.json +12 -7
- package/scripts/headless-order-test.mjs +67 -0
- package/scripts/validate-themes-against-host.mjs +115 -0
- package/scripts/verify-package.mjs +79 -0
- package/scripts/verify.mjs +452 -0
package/lib/types/index.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 个性化全部走既有接缝,不注册快捷键、不注册命令、不拦截任何输入:
|
|
5
5
|
* - 接缝四(主题):内置三套粉色 JSON(pink-night / pink-day / pink-ansi),
|
|
6
6
|
* 启动时复制进 ~/.dsh-tui/themes/(仅缺失时,绝不覆盖用户已有文件);
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
7
|
+
* - 缓存背景跟随:安全应用已有的终端背景缓存,在昼樱/夜樱间切换;
|
|
8
|
+
* dsh-TUI 未提供插件终端查询接缝时,不直接读写 stdin、raw mode 或 OSC 11;
|
|
9
9
|
* - 接缝十一(状态行):输入框上方一行小装饰(✿ · 时钟 · 本轮轮数);
|
|
10
10
|
* - 接缝六(设置区块):/settings 里一个可编辑面板,即时生效。
|
|
11
11
|
*
|
|
@@ -81,17 +81,14 @@ export function apply(ctx, config = {}) {
|
|
|
81
81
|
// The /settings user layer (settings.yaml) overrides the cordis layer and
|
|
82
82
|
// lands live through scope.watch; both override the hardcoded defaults.
|
|
83
83
|
let effective = cordis;
|
|
84
|
-
// Background follow
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// mounts (the host gates its own mount on the same service, probed
|
|
89
|
-
// against 0.9.0), so the pref write decides this boot exactly like a sync
|
|
90
|
-
// write would, and toggling followSystem in /settings re-decides live.
|
|
84
|
+
// Background follow applies only an existing cache. dsh-TUI v0.9.2 exposes
|
|
85
|
+
// no plugin terminal-query seam, so a theme plugin must not compete with
|
|
86
|
+
// Ink's stdin reader or raw-mode lease. The /settings layer still determines
|
|
87
|
+
// whether the cached result may control this startup.
|
|
91
88
|
let followActive;
|
|
92
89
|
const dataDir = join(homeDir(), '.dsh-tui');
|
|
93
|
-
const
|
|
94
|
-
runFollowSystem(dataDir, message => {
|
|
90
|
+
const applyFollow = () => {
|
|
91
|
+
runFollowSystem(dataDir, () => followActive === true, message => {
|
|
95
92
|
ctx.logger.info(`dsh-tui-theme: ${message}`);
|
|
96
93
|
});
|
|
97
94
|
};
|
|
@@ -100,23 +97,16 @@ export function apply(ctx, config = {}) {
|
|
|
100
97
|
if (effective.followSystem !== followActive) {
|
|
101
98
|
followActive = effective.followSystem;
|
|
102
99
|
if (followActive) {
|
|
103
|
-
|
|
100
|
+
applyFollow();
|
|
104
101
|
}
|
|
105
102
|
else {
|
|
106
103
|
ctx.logger.info('dsh-tui-theme: follow: disabled, manual /theme choice preserved');
|
|
107
104
|
}
|
|
108
105
|
}
|
|
109
106
|
});
|
|
110
|
-
//
|
|
111
|
-
// the
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// stays inside the host's own 300ms pre-mount settings gate either way.
|
|
115
|
-
setTimeout(() => {
|
|
116
|
-
if (followActive === undefined && cordis.followSystem) {
|
|
117
|
-
followActive = true;
|
|
118
|
-
startFollow();
|
|
119
|
-
}
|
|
120
|
-
}, FOLLOW_FALLBACK_MS);
|
|
107
|
+
// Cached following needs the merged settings document; without the settings
|
|
108
|
+
// seam the plugin keeps the user's existing theme choice and degrades to
|
|
109
|
+
// static assets rather than applying a profile default before that document
|
|
110
|
+
// could arrive.
|
|
121
111
|
startStatusLine(ctx, () => effective);
|
|
122
112
|
}
|
|
@@ -20,7 +20,7 @@ import type { StatusOptions } from './statusLine.js';
|
|
|
20
20
|
export type PinkSettingsDoc = StatusOptions & {
|
|
21
21
|
/** Install bundled theme JSONs on boot (cordis-config layer only). */
|
|
22
22
|
autoInstallThemes?: boolean;
|
|
23
|
-
/**
|
|
23
|
+
/** Apply a cached terminal background: pink-day <-> pink-night. */
|
|
24
24
|
followSystem?: boolean;
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settingsSection.d.ts","sourceRoot":"","sources":["../../src/settingsSection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,+EAA+E;AAC/E,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG;IAC5C,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,
|
|
1
|
+
{"version":3,"file":"settingsSection.d.ts","sourceRoot":"","sources":["../../src/settingsSection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,+EAA+E;AAC/E,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG;IAC5C,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAcD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,GACpC,IAAI,CAoDN"}
|
|
@@ -30,29 +30,36 @@ import z from '@deepseek-ai/schemastery';
|
|
|
30
30
|
export function registerPinkSettings(ctx, cordis, onDoc) {
|
|
31
31
|
ctx.inject(['settings'], settingsCtx => {
|
|
32
32
|
const settings = settingsCtx.settings;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
33
|
+
try {
|
|
34
|
+
const scope = settings.register(settingsNamespace('dsh-tui-theme'), z.object({
|
|
35
|
+
followSystem: z.boolean(),
|
|
36
|
+
showGlyph: z.boolean(),
|
|
37
|
+
showClock: z.boolean(),
|
|
38
|
+
showTurns: z.boolean(),
|
|
39
|
+
statusScope: z.union(['pink-only', 'all-themes']),
|
|
40
|
+
}));
|
|
41
|
+
const emit = (doc) => {
|
|
42
|
+
if (doc === null || typeof doc !== 'object')
|
|
43
|
+
return;
|
|
44
|
+
const clean = {};
|
|
45
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
46
|
+
if (value !== undefined)
|
|
47
|
+
clean[key] = value;
|
|
48
|
+
}
|
|
49
|
+
onDoc(clean);
|
|
50
|
+
};
|
|
51
|
+
// Own the watcher on the inject-scoped ledger so it survives exactly as
|
|
52
|
+
// long as this activation (scope.watch's disposer is otherwise leaked).
|
|
53
|
+
settingsCtx.effect(() => {
|
|
54
|
+
emit(scope.get());
|
|
55
|
+
return scope.watch(emit);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// A duplicate registration (hot reload race) or a stricter host must
|
|
60
|
+
// not take the plugin — or the TUI — down.
|
|
61
|
+
settingsCtx.logger.warn(`dsh-tui-pink-theme: settings namespace registration failed: ${String(error)}`);
|
|
62
|
+
}
|
|
56
63
|
});
|
|
57
64
|
ctx.inject(['tuiSettingsSections'], sectionsCtx => {
|
|
58
65
|
const sections = sectionsCtx
|
|
@@ -77,11 +84,11 @@ function sectionDefinition(cordis) {
|
|
|
77
84
|
fields: [
|
|
78
85
|
{
|
|
79
86
|
path: ['followSystem'],
|
|
80
|
-
label: '
|
|
81
|
-
descriptions: { zh: '
|
|
82
|
-
hint: '
|
|
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.',
|
|
83
90
|
hintDescriptions: {
|
|
84
|
-
zh: '
|
|
91
|
+
zh: '启动时应用此前保存的终端背景结果。dsh-TUI 0.9.2 未提供安全的插件查询接缝,因此本插件不会刷新该缓存。',
|
|
85
92
|
},
|
|
86
93
|
kind: 'boolean',
|
|
87
94
|
format: (value) => String(value ?? cordis.followSystem),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAOlD,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;AAEpD,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,sCAAsC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,iEAAiE;IACjE,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED,mCAAmC;AACnC,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAA;AAwCrD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAOlD,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;AAEpD,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,sCAAsC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,iEAAiE;IACjE,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED,mCAAmC;AACnC,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAA;AAwCrD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,GAAG,IAAI,CA8DvF"}
|
package/lib/types/statusLine.js
CHANGED
|
@@ -61,6 +61,7 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
61
61
|
const turns = new Map();
|
|
62
62
|
let current;
|
|
63
63
|
let dispose;
|
|
64
|
+
let lastText;
|
|
64
65
|
const render = () => {
|
|
65
66
|
try {
|
|
66
67
|
const eff = getEffective();
|
|
@@ -79,6 +80,9 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
const text = parts.join(' · ');
|
|
83
|
+
if (text === lastText)
|
|
84
|
+
return;
|
|
85
|
+
lastText = text;
|
|
82
86
|
// The trailing identity must be the inject-scoped context (the same
|
|
83
87
|
// activation the traceable binds as caller) — the plugin's outer ctx
|
|
84
88
|
// is a different activation view and would be silently rejected.
|
|
@@ -88,14 +92,14 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
88
92
|
// Display garnish only: a rendering hiccup must never travel upward.
|
|
89
93
|
}
|
|
90
94
|
};
|
|
91
|
-
|
|
95
|
+
statusCtx.on('session/event', (session, event) => {
|
|
92
96
|
current = session;
|
|
93
97
|
if (event?.type === 'turn/end') {
|
|
94
98
|
turns.set(session, (turns.get(session) ?? 0) + 1);
|
|
95
99
|
}
|
|
96
100
|
render();
|
|
97
101
|
});
|
|
98
|
-
|
|
102
|
+
statusCtx.on('session/disposed', session => {
|
|
99
103
|
turns.delete(session);
|
|
100
104
|
if (current === session)
|
|
101
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,
|
|
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"}
|
package/lib/types/themeAssets.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
55
|
-
|
|
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.
|
|
4
|
-
"description": "Sakura-pink themes for dsh-TUI with
|
|
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,7 +25,11 @@
|
|
|
24
25
|
},
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "tsc -p tsconfig.json",
|
|
27
|
-
"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"
|
|
28
33
|
},
|
|
29
34
|
"license": "MIT",
|
|
30
35
|
"author": "xiaoxiaohaigui",
|
|
@@ -50,19 +55,19 @@
|
|
|
50
55
|
"patch": "./cordis.patch.yml"
|
|
51
56
|
}
|
|
52
57
|
},
|
|
53
|
-
"dependencies": {
|
|
54
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
55
|
-
},
|
|
56
58
|
"peerDependencies": {
|
|
57
59
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
58
60
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1",
|
|
59
|
-
"@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"
|
|
60
63
|
},
|
|
61
64
|
"devDependencies": {
|
|
62
65
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
63
66
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1",
|
|
64
67
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1",
|
|
68
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
65
69
|
"@types/node": "^22.0.0",
|
|
70
|
+
"tsx": "^4.23.12",
|
|
66
71
|
"typescript": "^6.0.3"
|
|
67
72
|
}
|
|
68
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`)
|