dsh-tui-theme 0.3.2 → 0.5.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 +49 -25
- package/cordis.patch.yml +3 -3
- package/docs/screenshots/settings.png +0 -0
- package/lib/types/autoTheme.d.ts +16 -50
- package/lib/types/autoTheme.d.ts.map +1 -1
- package/lib/types/autoTheme.js +40 -176
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +36 -42
- package/lib/types/pluginId.d.ts +11 -0
- package/lib/types/pluginId.d.ts.map +1 -0
- package/lib/types/pluginId.js +10 -0
- package/lib/types/settingsSection.d.ts +2 -2
- package/lib/types/settingsSection.d.ts.map +1 -1
- package/lib/types/settingsSection.js +10 -9
- package/lib/types/statusLine.d.ts +1 -1
- package/lib/types/statusLine.d.ts.map +1 -1
- package/lib/types/statusLine.js +12 -4
- package/lib/types/themeAssets.d.ts +8 -3
- package/lib/types/themeAssets.d.ts.map +1 -1
- package/lib/types/themeAssets.js +59 -13
- package/package.json +12 -7
- package/scripts/expected-settings-contract.mjs +53 -0
- package/scripts/headless-order-test.mjs +132 -0
- package/scripts/validate-themes-against-host.mjs +199 -0
- package/scripts/verify-package.mjs +82 -0
- package/scripts/verify.mjs +545 -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
|
*
|
|
@@ -19,7 +19,8 @@ import { installBundledThemes, homeDir } from './themeAssets.js';
|
|
|
19
19
|
import { startStatusLine } from './statusLine.js';
|
|
20
20
|
import { runFollowSystem } from './autoTheme.js';
|
|
21
21
|
import { registerPinkSettings } from './settingsSection.js';
|
|
22
|
-
|
|
22
|
+
import { PLUGIN_ID } from './pluginId.js';
|
|
23
|
+
export const name = PLUGIN_ID;
|
|
23
24
|
// Explicit annotation: the inferred z.dict output references cosmokit's Dict
|
|
24
25
|
// through a pnpm-virtual path, which is not portable in declaration emit
|
|
25
26
|
// (TS2883). Mirrors the official plugin-template's workaround.
|
|
@@ -46,12 +47,6 @@ const DEFAULTS = {
|
|
|
46
47
|
showTurns: true,
|
|
47
48
|
statusScope: 'pink-only',
|
|
48
49
|
};
|
|
49
|
-
/**
|
|
50
|
-
* Backstop delay for the cordis-layer follow decision on hosts without a
|
|
51
|
-
* settings service (see apply). Stays under the host's own 300ms pre-mount
|
|
52
|
-
* settings gate so the pref write still lands before first paint.
|
|
53
|
-
*/
|
|
54
|
-
const FOLLOW_FALLBACK_MS = 150;
|
|
55
50
|
/**
|
|
56
51
|
* Wire the pink theme plugin.
|
|
57
52
|
* @param ctx - Cordis context (the plugin's own activation).
|
|
@@ -72,56 +67,55 @@ export function apply(ctx, config = {}) {
|
|
|
72
67
|
if (cordis.autoInstallThemes) {
|
|
73
68
|
const result = installBundledThemes();
|
|
74
69
|
for (const file of result.installed) {
|
|
75
|
-
ctx.logger.info(
|
|
70
|
+
ctx.logger.info(`${PLUGIN_ID}: installed bundled theme "${file}" into ~/.dsh-tui/themes/`);
|
|
71
|
+
}
|
|
72
|
+
for (const file of result.repaired) {
|
|
73
|
+
ctx.logger.warn(`${PLUGIN_ID}: found a corrupt "${file}" in ~/.dsh-tui/themes/, backed it up and reinstalled the bundled copy`);
|
|
76
74
|
}
|
|
77
75
|
for (const file of result.failed) {
|
|
78
|
-
ctx.logger.warn(
|
|
76
|
+
ctx.logger.warn(`${PLUGIN_ID}: could not install bundled theme "${file}"`);
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
79
|
// The /settings user layer (settings.yaml) overrides the cordis layer and
|
|
82
80
|
// lands live through scope.watch; both override the hardcoded defaults.
|
|
83
81
|
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.
|
|
82
|
+
// Background follow applies only an existing cache. dsh-TUI exposes
|
|
83
|
+
// no plugin terminal-query seam, so a theme plugin must not compete with
|
|
84
|
+
// Ink's stdin reader or raw-mode lease. The /settings layer still determines
|
|
85
|
+
// whether the cached result may control this startup.
|
|
91
86
|
let followActive;
|
|
92
87
|
const dataDir = join(homeDir(), '.dsh-tui');
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const startFollow = () => {
|
|
97
|
-
runFollowSystem(dataDir, followEnabled, message => {
|
|
98
|
-
ctx.logger.info(`dsh-tui-theme: ${message}`);
|
|
88
|
+
const applyFollow = () => {
|
|
89
|
+
runFollowSystem(dataDir, () => followActive === true, message => {
|
|
90
|
+
ctx.logger.info(`${PLUGIN_ID}: ${message}`);
|
|
99
91
|
});
|
|
100
92
|
};
|
|
101
93
|
registerPinkSettings(ctx, cordis, doc => {
|
|
102
94
|
effective = { ...cordis, ...doc };
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
95
|
+
const follow = effective.followSystem === true;
|
|
96
|
+
if (followActive === undefined) {
|
|
97
|
+
// First document: align the baseline, not a switch. A value that only
|
|
98
|
+
// matches the default logs nothing; a user layer that starts enabled
|
|
99
|
+
// still applies the cache immediately.
|
|
100
|
+
followActive = follow;
|
|
101
|
+
if (follow)
|
|
102
|
+
applyFollow();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (followActive !== follow) {
|
|
106
|
+
followActive = follow;
|
|
107
|
+
if (follow) {
|
|
108
|
+
applyFollow();
|
|
107
109
|
}
|
|
108
110
|
else {
|
|
109
|
-
ctx.logger.info(
|
|
111
|
+
ctx.logger.info(`${PLUGIN_ID}: follow: disabled, manual /theme choice preserved`);
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
});
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
const fallbackTimer = setTimeout(() => {
|
|
119
|
-
if (followActive === undefined && cordis.followSystem) {
|
|
120
|
-
followActive = true;
|
|
121
|
-
startFollow();
|
|
122
|
-
}
|
|
123
|
-
}, FOLLOW_FALLBACK_MS);
|
|
124
|
-
fallbackTimer.unref?.();
|
|
125
|
-
ctx.effect(() => () => clearTimeout(fallbackTimer));
|
|
115
|
+
// The follow decision is owned entirely by the /settings layer above: there
|
|
116
|
+
// is no timer or fallback path on hosts without a settings service — the
|
|
117
|
+
// plugin keeps the user's existing theme choice and degrades to static
|
|
118
|
+
// assets rather than applying a profile default before the merged document
|
|
119
|
+
// can arrive.
|
|
126
120
|
startStatusLine(ctx, () => effective);
|
|
127
121
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single plugin identifier shared by every surface that names this
|
|
3
|
+
* plugin to the host: the cordis plugin name, the tuiStatus contribution
|
|
4
|
+
* key, and the /settings namespace. These are distinct roles that happen
|
|
5
|
+
* to carry one value — all three import this constant rather than repeating
|
|
6
|
+
* the literal, so a rename stays consistent and the test contract (which
|
|
7
|
+
* re-derives the value from the package name) stays meaningful.
|
|
8
|
+
* @module dsh-tui-theme/pluginId
|
|
9
|
+
*/
|
|
10
|
+
export declare const PLUGIN_ID = "dsh-tui-theme";
|
|
11
|
+
//# sourceMappingURL=pluginId.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pluginId.d.ts","sourceRoot":"","sources":["../../src/pluginId.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single plugin identifier shared by every surface that names this
|
|
3
|
+
* plugin to the host: the cordis plugin name, the tuiStatus contribution
|
|
4
|
+
* key, and the /settings namespace. These are distinct roles that happen
|
|
5
|
+
* to carry one value — all three import this constant rather than repeating
|
|
6
|
+
* the literal, so a rename stays consistent and the test contract (which
|
|
7
|
+
* re-derives the value from the package name) stays meaningful.
|
|
8
|
+
* @module dsh-tui-theme/pluginId
|
|
9
|
+
*/
|
|
10
|
+
export const PLUGIN_ID = 'dsh-tui-theme';
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Both services are consumed through `ctx.inject`, not apply-time `get`
|
|
13
13
|
* probes: this row may start before the host's service rows, and the inject
|
|
14
14
|
* fires whenever each service actually registers.
|
|
15
|
-
* @module dsh-tui-
|
|
15
|
+
* @module dsh-tui-theme/settingsSection
|
|
16
16
|
*/
|
|
17
17
|
import type { Context } from '@deepseek-ai/cordis';
|
|
18
18
|
import type { StatusOptions } from './statusLine.js';
|
|
@@ -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;
|
|
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;AAGpD,+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"}
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* Both services are consumed through `ctx.inject`, not apply-time `get`
|
|
13
13
|
* probes: this row may start before the host's service rows, and the inject
|
|
14
14
|
* fires whenever each service actually registers.
|
|
15
|
-
* @module dsh-tui-
|
|
15
|
+
* @module dsh-tui-theme/settingsSection
|
|
16
16
|
*/
|
|
17
17
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
18
18
|
import z from '@deepseek-ai/schemastery';
|
|
19
|
+
import { PLUGIN_ID } from './pluginId.js';
|
|
19
20
|
/**
|
|
20
21
|
* Register the settings namespace (mirror the resolved document to the
|
|
21
22
|
* caller) and, separately, the /settings section for it. Each part waits for
|
|
@@ -31,7 +32,7 @@ export function registerPinkSettings(ctx, cordis, onDoc) {
|
|
|
31
32
|
ctx.inject(['settings'], settingsCtx => {
|
|
32
33
|
const settings = settingsCtx.settings;
|
|
33
34
|
try {
|
|
34
|
-
const scope = settings.register(settingsNamespace(
|
|
35
|
+
const scope = settings.register(settingsNamespace(PLUGIN_ID), z.object({
|
|
35
36
|
followSystem: z.boolean(),
|
|
36
37
|
showGlyph: z.boolean(),
|
|
37
38
|
showClock: z.boolean(),
|
|
@@ -58,7 +59,7 @@ export function registerPinkSettings(ctx, cordis, onDoc) {
|
|
|
58
59
|
catch (error) {
|
|
59
60
|
// A duplicate registration (hot reload race) or a stricter host must
|
|
60
61
|
// not take the plugin — or the TUI — down.
|
|
61
|
-
settingsCtx.logger.warn(`dsh-tui-
|
|
62
|
+
settingsCtx.logger.warn(`dsh-tui-theme: settings namespace registration failed: ${String(error)}`);
|
|
62
63
|
}
|
|
63
64
|
});
|
|
64
65
|
ctx.inject(['tuiSettingsSections'], sectionsCtx => {
|
|
@@ -71,24 +72,24 @@ export function registerPinkSettings(ctx, cordis, onDoc) {
|
|
|
71
72
|
catch (error) {
|
|
72
73
|
// A duplicate registration (hot reload race) or a stricter host must
|
|
73
74
|
// not take the plugin — or the TUI — down.
|
|
74
|
-
sectionsCtx.logger.warn(`dsh-tui-
|
|
75
|
+
sectionsCtx.logger.warn(`dsh-tui-theme: settings section registration failed: ${String(error)}`);
|
|
75
76
|
}
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
79
|
/** The declarative /settings block (labels bilingual, zh via descriptions). */
|
|
79
80
|
function sectionDefinition(cordis) {
|
|
80
81
|
return {
|
|
81
|
-
ns:
|
|
82
|
+
ns: PLUGIN_ID,
|
|
82
83
|
title: 'pink-theme',
|
|
83
84
|
descriptions: { zh: 'pink-theme' },
|
|
84
85
|
fields: [
|
|
85
86
|
{
|
|
86
87
|
path: ['followSystem'],
|
|
87
|
-
label: '
|
|
88
|
-
descriptions: { zh: '
|
|
89
|
-
hint: '
|
|
88
|
+
label: 'Apply saved terminal background',
|
|
89
|
+
descriptions: { zh: '应用上次保存的终端背景' },
|
|
90
|
+
hint: 'Apply a previously saved terminal background result at startup. dsh-TUI does not expose a safe plugin query, so this plugin does not refresh the cache.',
|
|
90
91
|
hintDescriptions: {
|
|
91
|
-
zh: '
|
|
92
|
+
zh: '启动时应用此前保存的终端背景结果。dsh-TUI 未提供安全的插件查询接缝,因此本插件不会刷新该缓存。',
|
|
92
93
|
},
|
|
93
94
|
kind: 'boolean',
|
|
94
95
|
format: (value) => String(value ?? cordis.followSystem),
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* pink theme is active (checked per render with the host's own theme
|
|
11
11
|
* precedence, so a mid-session /theme switch takes effect on the next tick);
|
|
12
12
|
* `statusScope: 'all-themes'` opts it into every other theme too.
|
|
13
|
-
* @module dsh-tui-
|
|
13
|
+
* @module dsh-tui-theme/statusLine
|
|
14
14
|
*/
|
|
15
15
|
import type { Context } from '@deepseek-ai/cordis';
|
|
16
16
|
/** Which themes the blossom line renders under. */
|
|
@@ -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;
|
|
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;AAQlD,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;AA+CrD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,GAAG,IAAI,CA8DvF"}
|
package/lib/types/statusLine.js
CHANGED
|
@@ -10,13 +10,16 @@
|
|
|
10
10
|
* pink theme is active (checked per render with the host's own theme
|
|
11
11
|
* precedence, so a mid-session /theme switch takes effect on the next tick);
|
|
12
12
|
* `statusScope: 'all-themes'` opts it into every other theme too.
|
|
13
|
-
* @module dsh-tui-
|
|
13
|
+
* @module dsh-tui-theme/statusLine
|
|
14
14
|
*/
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { homeDir } from './themeAssets.js';
|
|
17
17
|
import { readThemePref } from './autoTheme.js';
|
|
18
|
+
import { PLUGIN_ID } from './pluginId.js';
|
|
18
19
|
const GLYPH = '✿';
|
|
19
|
-
|
|
20
|
+
// The tuiStatus contribution key (same value as the settings namespace and
|
|
21
|
+
// the cordis plugin name — one literal would be three drift risks).
|
|
22
|
+
const STATUS_KEY = PLUGIN_ID;
|
|
20
23
|
const CLOCK_TICK_MS = 15_000;
|
|
21
24
|
/** The bundled themes this garnish belongs to. */
|
|
22
25
|
const PINK_THEMES = new Set(['pink-night', 'pink-day', 'pink-ansi']);
|
|
@@ -25,6 +28,11 @@ const PINK_THEMES = new Set(['pink-night', 'pink-day', 'pink-ansi']);
|
|
|
25
28
|
* then the persisted ~/.dsh-tui/theme.json pref. The unforced path (OSC 11
|
|
26
29
|
* auto-detection) only ever resolves to a builtin palette, never a pink one,
|
|
27
30
|
* so "no pref" means non-pink.
|
|
31
|
+
*
|
|
32
|
+
* This deliberately mirrors the host's ThemeProvider resolution chain
|
|
33
|
+
* (`components/design-system/ThemeProvider.tsx`, baseline dsh-TUI 0.9.3);
|
|
34
|
+
* keep the two in sync if the host adds a precedence layer. If the host ever
|
|
35
|
+
* exposes a theme-query seam for plugins, prefer that over this re-read.
|
|
28
36
|
*/
|
|
29
37
|
function activeThemeName(dataDir) {
|
|
30
38
|
const env = process.env.DSH_TUI_THEME;
|
|
@@ -92,14 +100,14 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
92
100
|
// Display garnish only: a rendering hiccup must never travel upward.
|
|
93
101
|
}
|
|
94
102
|
};
|
|
95
|
-
|
|
103
|
+
statusCtx.on('session/event', (session, event) => {
|
|
96
104
|
current = session;
|
|
97
105
|
if (event?.type === 'turn/end') {
|
|
98
106
|
turns.set(session, (turns.get(session) ?? 0) + 1);
|
|
99
107
|
}
|
|
100
108
|
render();
|
|
101
109
|
});
|
|
102
|
-
|
|
110
|
+
statusCtx.on('session/disposed', session => {
|
|
103
111
|
turns.delete(session);
|
|
104
112
|
if (current === session)
|
|
105
113
|
current = undefined;
|
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Copies the package's themes/*.json into ~/.dsh-tui/themes/ on boot. Only
|
|
5
5
|
* files that do not exist yet are written — a user's edited or same-named
|
|
6
|
-
* theme file is never overwritten.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* theme file is never overwritten. The one exception is a target that no
|
|
7
|
+
* longer parses as JSON (a torn write from an interrupted installation):
|
|
8
|
+
* that file is backed up under a .corrupt-<timestamp> name and replaced, so
|
|
9
|
+
* a crash can never shadow a bundled theme forever. Every failure is
|
|
10
|
+
* contained per file: a theme garnish must never break the TUI's boot.
|
|
11
|
+
* @module dsh-tui-theme/themeAssets
|
|
9
12
|
*/
|
|
10
13
|
export interface ThemeInstallResult {
|
|
11
14
|
/** Files newly written into the target directory. */
|
|
12
15
|
readonly installed: readonly string[];
|
|
13
16
|
/** Files already present in the target directory (left untouched). */
|
|
14
17
|
readonly skipped: readonly string[];
|
|
18
|
+
/** Corrupt targets backed up and reinstalled (self-heal). */
|
|
19
|
+
readonly repaired: readonly string[];
|
|
15
20
|
/** Files that could not be installed (per-file failures). */
|
|
16
21
|
readonly failed: readonly string[];
|
|
17
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"themeAssets.d.ts","sourceRoot":"","sources":["../../src/themeAssets.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"themeAssets.d.ts","sourceRoot":"","sources":["../../src/themeAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;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,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,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,CAiCpB"}
|
package/lib/types/themeAssets.js
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Copies the package's themes/*.json into ~/.dsh-tui/themes/ on boot. Only
|
|
5
5
|
* files that do not exist yet are written — a user's edited or same-named
|
|
6
|
-
* theme file is never overwritten.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* theme file is never overwritten. The one exception is a target that no
|
|
7
|
+
* longer parses as JSON (a torn write from an interrupted installation):
|
|
8
|
+
* that file is backed up under a .corrupt-<timestamp> name and replaced, so
|
|
9
|
+
* a crash can never shadow a bundled theme forever. Every failure is
|
|
10
|
+
* contained per file: a theme garnish must never break the TUI's boot.
|
|
11
|
+
* @module dsh-tui-theme/themeAssets
|
|
9
12
|
*/
|
|
10
|
-
import {
|
|
13
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
11
14
|
import { homedir } from 'node:os';
|
|
12
15
|
import { dirname, join } from 'node:path';
|
|
13
16
|
import { fileURLToPath } from 'node:url';
|
|
@@ -33,30 +36,73 @@ export function themesTargetDir() {
|
|
|
33
36
|
export function installBundledThemes(targetDir = themesTargetDir(), sourceDir = bundledThemesDir()) {
|
|
34
37
|
const installed = [];
|
|
35
38
|
const skipped = [];
|
|
39
|
+
const repaired = [];
|
|
36
40
|
const failed = [];
|
|
37
41
|
let files;
|
|
38
42
|
try {
|
|
39
43
|
files = readdirSync(sourceDir).filter(entry => entry.toLowerCase().endsWith('.json'));
|
|
40
44
|
}
|
|
41
45
|
catch {
|
|
42
|
-
return { installed, skipped, failed: [sourceDir] };
|
|
46
|
+
return { installed, skipped, repaired, failed: [sourceDir] };
|
|
43
47
|
}
|
|
44
48
|
for (const file of files) {
|
|
49
|
+
const target = join(targetDir, file);
|
|
45
50
|
try {
|
|
46
|
-
const target = join(targetDir, file);
|
|
47
|
-
if (existsSync(target)) {
|
|
48
|
-
skipped.push(file);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
51
|
const text = readFileSync(join(sourceDir, file), 'utf8');
|
|
52
52
|
JSON.parse(text); // our own asset, but never write a corrupt file out
|
|
53
53
|
mkdirSync(targetDir, { recursive: true });
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
try {
|
|
55
|
+
writeFileSync(target, text, { flag: 'wx' });
|
|
56
|
+
installed.push(file);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code === 'EEXIST') {
|
|
60
|
+
if (healCorruptTarget(target, text))
|
|
61
|
+
repaired.push(file);
|
|
62
|
+
else
|
|
63
|
+
skipped.push(file);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
failed.push(file);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
}
|
|
57
70
|
catch {
|
|
58
71
|
failed.push(file);
|
|
59
72
|
}
|
|
60
73
|
}
|
|
61
|
-
return { installed, skipped, failed };
|
|
74
|
+
return { installed, skipped, repaired, failed };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Self-heal an existing target that fails to parse as JSON — the leftover of
|
|
78
|
+
* a torn write from an interrupted installation. The damaged file is kept as
|
|
79
|
+
* <target>.corrupt-<timestamp> and the bundled copy installed fresh. Returns
|
|
80
|
+
* false (leave untouched) when the target is valid JSON (a user file the
|
|
81
|
+
* never-overwrite rule protects), unreadable (unreadable is not proven
|
|
82
|
+
* corrupt), or when the backup/replace itself fails (degrade to the plain
|
|
83
|
+
* silent skip).
|
|
84
|
+
*/
|
|
85
|
+
function healCorruptTarget(target, text) {
|
|
86
|
+
let existing;
|
|
87
|
+
try {
|
|
88
|
+
existing = readFileSync(target, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
JSON.parse(existing);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Proven corrupt: fall through to backup and reinstall.
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
renameSync(target, `${target}.corrupt-${Date.now()}`);
|
|
102
|
+
writeFileSync(target, text, { flag: 'wx' });
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
62
108
|
}
|
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.5.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": {
|
|
@@ -25,7 +26,11 @@
|
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "tsc -p tsconfig.json",
|
|
27
28
|
"preverify": "npm run build",
|
|
28
|
-
"verify": "node scripts/verify.mjs"
|
|
29
|
+
"verify": "node scripts/verify.mjs",
|
|
30
|
+
"verify:package": "node scripts/verify-package.mjs",
|
|
31
|
+
"verify:host": "npm run build && node scripts/headless-order-test.mjs && node --import tsx/esm scripts/validate-themes-against-host.mjs",
|
|
32
|
+
"release:check": "npm run build && npm run verify && npm run verify:package",
|
|
33
|
+
"prepack": "npm run release:check"
|
|
29
34
|
},
|
|
30
35
|
"license": "MIT",
|
|
31
36
|
"author": "xiaoxiaohaigui",
|
|
@@ -51,19 +56,19 @@
|
|
|
51
56
|
"patch": "./cordis.patch.yml"
|
|
52
57
|
}
|
|
53
58
|
},
|
|
54
|
-
"dependencies": {
|
|
55
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
56
|
-
},
|
|
57
59
|
"peerDependencies": {
|
|
58
60
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
59
61
|
"@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"
|
|
62
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1",
|
|
63
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
61
64
|
},
|
|
62
65
|
"devDependencies": {
|
|
63
66
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
64
67
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1",
|
|
65
68
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1",
|
|
69
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
66
70
|
"@types/node": "^22.0.0",
|
|
71
|
+
"tsx": "^4.23.12",
|
|
67
72
|
"typescript": "^6.0.3"
|
|
68
73
|
}
|
|
69
74
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings contract this plugin publishes to the host's /settings form:
|
|
3
|
+
* the namespace, every field's path + kind, and the select options of the
|
|
4
|
+
* status scope. Both verification scripts assert against this single copy so
|
|
5
|
+
* a field addition or reorder only needs one edit here — the assertions are
|
|
6
|
+
* order-insensitive, so reordering the fields in sectionDefinition() is not
|
|
7
|
+
* an error by itself.
|
|
8
|
+
*
|
|
9
|
+
* The namespace is derived from the package name, mirroring PLUGIN_ID in
|
|
10
|
+
* src/pluginId.ts: if the package and its registration id ever drift apart,
|
|
11
|
+
* the headless order test fails on the namespace comparison instead of
|
|
12
|
+
* silently testing the wrong section.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from 'node:fs'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
|
|
17
|
+
export const SETTINGS_NAMESPACE = JSON.parse(
|
|
18
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
19
|
+
).name
|
|
20
|
+
|
|
21
|
+
/** [path, kind] pairs; compare as a set, never positionally. */
|
|
22
|
+
export const SETTINGS_FIELDS = [
|
|
23
|
+
[['followSystem'], 'boolean'],
|
|
24
|
+
[['showGlyph'], 'boolean'],
|
|
25
|
+
[['showClock'], 'boolean'],
|
|
26
|
+
[['showTurns'], 'boolean'],
|
|
27
|
+
[['statusScope'], 'select'],
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/** The values the statusScope select must expose, looked up by field path. */
|
|
31
|
+
export const STATUS_SCOPE_FIELD_PATH = ['statusScope']
|
|
32
|
+
export const STATUS_SCOPE_OPTIONS = ['pink-only', 'all-themes']
|
|
33
|
+
|
|
34
|
+
/** Assert a section object (ns + fields) matches the contract. */
|
|
35
|
+
export function assertSettingsContract(assert, section) {
|
|
36
|
+
assert.equal(section.ns, SETTINGS_NAMESPACE, 'settings namespace must match')
|
|
37
|
+
const actual = section.fields.map(field => [field.path, field.kind])
|
|
38
|
+
const expected = SETTINGS_FIELDS.map(([path, kind]) => `${JSON.stringify(path)}:${kind}`)
|
|
39
|
+
assert.deepEqual(
|
|
40
|
+
actual.map(([path, kind]) => `${JSON.stringify(path)}:${kind}`).sort(),
|
|
41
|
+
[...expected].sort(),
|
|
42
|
+
'settings fields must remain compatible with the host form contract',
|
|
43
|
+
)
|
|
44
|
+
const scopeField = section.fields.find(
|
|
45
|
+
field => JSON.stringify(field.path) === JSON.stringify(STATUS_SCOPE_FIELD_PATH),
|
|
46
|
+
)
|
|
47
|
+
assert.ok(scopeField, 'statusScope field must exist (looked up by path, not position)')
|
|
48
|
+
assert.deepEqual(
|
|
49
|
+
scopeField.options?.map(option => option.value),
|
|
50
|
+
STATUS_SCOPE_OPTIONS,
|
|
51
|
+
'status scope must expose both supported select values',
|
|
52
|
+
)
|
|
53
|
+
}
|