dsh-tui-theme 0.7.0 → 0.7.1
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 +7 -2
- package/lib/types/autoTheme.d.ts +3 -0
- package/lib/types/autoTheme.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +3 -0
- package/lib/types/statusLine.d.ts.map +1 -1
- package/lib/types/statusLine.js +17 -13
- package/lib/types/themeAssets.d.ts +4 -0
- package/lib/types/themeAssets.d.ts.map +1 -1
- package/lib/types/themeAssets.js +8 -3
- package/package.json +3 -3
- package/scripts/runtime-themes-headless.mjs +75 -0
- package/scripts/validate-themes-against-host.mjs +7 -0
- package/scripts/verify-package.mjs +10 -7
- package/scripts/verify.mjs +25 -0
package/README.md
CHANGED
|
@@ -150,7 +150,7 @@ rm ~/.dsh-tui/theme-follow.json
|
|
|
150
150
|
## 开发
|
|
151
151
|
|
|
152
152
|
```sh
|
|
153
|
-
npm install
|
|
153
|
+
npm install --include=dev # 见下方安装前提,本仓不提交 lockfile
|
|
154
154
|
npm run build
|
|
155
155
|
npm run verify
|
|
156
156
|
npm run verify:package
|
|
@@ -159,7 +159,12 @@ DSH_TUI_SOURCE_ROOT=/path/to/dsh-TUI-source \
|
|
|
159
159
|
npm run verify:host
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
`
|
|
162
|
+
安装前提(这两条会让普通 `npm install` 失败或静默不装依赖):
|
|
163
|
+
|
|
164
|
+
- **本仓有意不提交 `package-lock.json`。** devDependency 里的宿主 tarball 内嵌 `@dsh-std/*`,其包内 manifest 仍声明 `workspace:*`;提交的 lockfile 会被 npm 忠实重放,于是 `npm install` 与 `npm ci` 都报 `EUNSUPPORTEDPROTOCOL`。删掉 lockfile 后 npm 会直接跳过 tarball 内的 bundle 目录,正常安装。请勿把 lockfile 提交回来(CI 的 `contract` 档会直接拦下)。
|
|
165
|
+
- **安装时不要带 `NODE_ENV=production`。** 该变量会让 npm 按 `omit=dev` 静默省略 devDependencies,表现为 `up to date` 但 `node_modules` 为空、随后 `tsc` 找不到。所以显式写 `--include=dev`(CI 里另外固定 `NODE_ENV=development`)。
|
|
166
|
+
|
|
167
|
+
`verify:host` 默认使用开发依赖中的 dsh-TUI(当前为 0.10.1)进行零配置验证,无需本地宿主源码检出;需要验证旧版或发布基线时,再显式指向同一版本的宿主 adapter 与源码。需要锁定版本时,额外设置 `DSH_TUI_EXPECTED_VERSION`。CI 只覆盖这条 0.10.1 零配置线,0.9.x 兼容回归仍需本机按上面的 env 指向对应宿主 worktree。
|
|
163
168
|
|
|
164
169
|
主题调色板改起来最直接:编辑 `themes/*.json` 后重新 `npm run verify`,再删掉 `~/.dsh-tui/themes/` 下对应文件让插件重装。
|
|
165
170
|
|
package/lib/types/autoTheme.d.ts
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
interface FollowCache {
|
|
15
15
|
light: boolean;
|
|
16
|
+
/** Diagnostic only (the /settings panel shows its date); never an expiry
|
|
17
|
+
* signal — the cache is applied as-is whenever the user opts in, and this
|
|
18
|
+
* plugin has no producer to refresh it (see the module header). */
|
|
16
19
|
at: number;
|
|
17
20
|
}
|
|
18
21
|
/** The persisted theme pref ({ theme: name }), host format. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autoTheme.d.ts","sourceRoot":"","sources":["../../src/autoTheme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,UAAU,WAAW;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;CACX;AAsCD,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjE;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGxE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE;AAED,oFAAoF;AACpF,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAA;AAE3B;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,OAAO,EACxB,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,GACvC,IAAI,CAUN"}
|
|
1
|
+
{"version":3,"file":"autoTheme.d.ts","sourceRoot":"","sources":["../../src/autoTheme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,UAAU,WAAW;IACnB,KAAK,EAAE,OAAO,CAAA;IACd;;wEAEoE;IACpE,EAAE,EAAE,MAAM,CAAA;CACX;AAsCD,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjE;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGxE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE;AAED,oFAAoF;AACpF,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAA;AAE3B;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,OAAO,EACxB,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,GACvC,IAAI,CAUN"}
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAKjF,eAAO,MAAM,IAAI,kBAAY,CAAA;AAE7B,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,eAAe,CAAA;AAUpC,eAAO,MAAM,MAAM,EAAE,WAAW,CAAC,MAAM,CAUrC,CAAA;AAoBF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAKjF,eAAO,MAAM,IAAI,kBAAY,CAAA;AAE7B,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,eAAe,CAAA;AAUpC,eAAO,MAAM,MAAM,EAAE,WAAW,CAAC,MAAM,CAUrC,CAAA;AAoBF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CAoK7D"}
|
package/lib/types/index.js
CHANGED
|
@@ -94,6 +94,9 @@ export function apply(ctx, config = {}) {
|
|
|
94
94
|
if (!cordis.autoInstallThemes)
|
|
95
95
|
return;
|
|
96
96
|
const result = installBundledThemes();
|
|
97
|
+
if (result.sourceError !== undefined) {
|
|
98
|
+
ctx.logger.warn(`${PLUGIN_ID}: ${result.sourceError}`);
|
|
99
|
+
}
|
|
97
100
|
for (const file of result.installed) {
|
|
98
101
|
ctx.logger.info(`${PLUGIN_ID}: installed bundled theme "${file}" into ~/.dsh-tui/themes/`);
|
|
99
102
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAkBlD,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;AAEpD,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,4CAA4C;IAC5C,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;AAsCrD;;;;;;GAMG;AACH,wBAAgB,gCAAgC,IAAI,IAAI,CAGvD;
|
|
1
|
+
{"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAkBlD,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;AAEpD,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,4CAA4C;IAC5C,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;AAsCrD;;;;;;GAMG;AACH,wBAAgB,gCAAgC,IAAI,IAAI,CAGvD;AAuGD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,GAAG,IAAI,CAyIvF"}
|
package/lib/types/statusLine.js
CHANGED
|
@@ -106,8 +106,7 @@ function activeThemeName(dataDir) {
|
|
|
106
106
|
}
|
|
107
107
|
return prefCache.value;
|
|
108
108
|
}
|
|
109
|
-
function
|
|
110
|
-
const name = activeThemeName(dataDir);
|
|
109
|
+
function isPinkTheme(name) {
|
|
111
110
|
return name !== undefined && PINK_THEMES.has(name);
|
|
112
111
|
}
|
|
113
112
|
// ── palette colors ──────────────────────────────────────────────────────────
|
|
@@ -151,13 +150,12 @@ function readPalette(name) {
|
|
|
151
150
|
}
|
|
152
151
|
return NO_COLORS;
|
|
153
152
|
}
|
|
154
|
-
function paletteFor(
|
|
155
|
-
|
|
156
|
-
if (name === undefined || !PINK_THEMES.has(name))
|
|
153
|
+
function paletteFor(themeName) {
|
|
154
|
+
if (!isPinkTheme(themeName))
|
|
157
155
|
return NO_COLORS;
|
|
158
156
|
const now = Date.now();
|
|
159
|
-
if (colorsCache === undefined || colorsCache.name !==
|
|
160
|
-
colorsCache = { at: now, name, colors: readPalette(
|
|
157
|
+
if (colorsCache === undefined || colorsCache.name !== themeName || now - colorsCache.at >= THEME_PREF_TTL_MS) {
|
|
158
|
+
colorsCache = { at: now, name: themeName, colors: readPalette(themeName) };
|
|
161
159
|
}
|
|
162
160
|
return colorsCache.colors;
|
|
163
161
|
}
|
|
@@ -192,7 +190,8 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
192
190
|
// differ in how the result reaches the host.
|
|
193
191
|
const renderScalar = () => {
|
|
194
192
|
const eff = getEffective();
|
|
195
|
-
const
|
|
193
|
+
const themeName = activeThemeName(dataDir);
|
|
194
|
+
const cells = statusCells(eff, themeName);
|
|
196
195
|
const separator = sanitizeOrnament(eff.statusSeparator, SEPARATOR);
|
|
197
196
|
const parts = [cells.glyph, cells.clock, cells.turns].filter((value) => value !== undefined);
|
|
198
197
|
const text = parts.join(` ${separator} `);
|
|
@@ -209,14 +208,18 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
209
208
|
try {
|
|
210
209
|
if (store !== undefined) {
|
|
211
210
|
const eff = getEffective();
|
|
212
|
-
|
|
211
|
+
// One pref read per render, shared by the visibility check and the
|
|
212
|
+
// palette: the module header's "one cheap stat" invariant holds on
|
|
213
|
+
// both paths (statusCells and paletteFor each used to re-resolve).
|
|
214
|
+
const themeName = activeThemeName(dataDir);
|
|
215
|
+
const cells = statusCells(eff, themeName);
|
|
213
216
|
store.push({
|
|
214
217
|
visible: cells.glyph !== undefined || cells.clock !== undefined || cells.turns !== undefined,
|
|
215
218
|
glyph: cells.glyph,
|
|
216
219
|
clock: cells.clock,
|
|
217
220
|
turns: cells.turns,
|
|
218
221
|
separator: sanitizeOrnament(eff.statusSeparator, SEPARATOR),
|
|
219
|
-
colors: paletteFor(
|
|
222
|
+
colors: paletteFor(themeName),
|
|
220
223
|
});
|
|
221
224
|
return;
|
|
222
225
|
}
|
|
@@ -227,9 +230,10 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
227
230
|
}
|
|
228
231
|
};
|
|
229
232
|
/** The three optional cells (all undefined = the line is off: master
|
|
230
|
-
* switch, theme scope, and the toggles fold into this one shape).
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
* switch, theme scope, and the toggles fold into this one shape).
|
|
234
|
+
* Takes the theme name resolved once by the caller per render. */
|
|
235
|
+
function statusCells(eff, themeName) {
|
|
236
|
+
const enabled = eff.statusEnabled && (eff.statusScope === 'all-themes' || isPinkTheme(themeName));
|
|
233
237
|
if (!enabled)
|
|
234
238
|
return { glyph: undefined, clock: undefined, turns: undefined };
|
|
235
239
|
return {
|
|
@@ -19,6 +19,10 @@ export interface ThemeInstallResult {
|
|
|
19
19
|
readonly repaired: readonly string[];
|
|
20
20
|
/** Files that could not be installed (per-file failures). */
|
|
21
21
|
readonly failed: readonly string[];
|
|
22
|
+
/** Set only when the bundled directory itself was unreadable, so nothing
|
|
23
|
+
* was attempted and no per-file entry exists. Carries the directory and
|
|
24
|
+
* the underlying error for the caller's log. */
|
|
25
|
+
readonly sourceError?: string;
|
|
22
26
|
}
|
|
23
27
|
export interface BundledTheme {
|
|
24
28
|
readonly file: string;
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;IAClC;;qDAEiD;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAA;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAOD,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,2DAA2D;AAC3D,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,MAA2B,GAAG,YAAY,EAAE,CA0CxF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,kBAAkB,CAqCpB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,MAAM,EAAE,CAuBV;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,MAAM,EAAE,CAkBV"}
|
package/lib/types/themeAssets.js
CHANGED
|
@@ -15,7 +15,10 @@ import { homedir } from 'node:os';
|
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
// Same resolution order as the host's utils/paths.ts homeDir(): os.homedir()
|
|
18
|
-
// first, USERPROFILE/HOME spellings as stripped-down fallbacks.
|
|
18
|
+
// first, USERPROFILE/HOME spellings as stripped-down fallbacks. When all three
|
|
19
|
+
// come up empty the returned '' makes every derived path resolve against the
|
|
20
|
+
// process cwd — the host's own semantics, kept deliberately identical rather
|
|
21
|
+
// than second-guessed here.
|
|
19
22
|
export function homeDir() {
|
|
20
23
|
return homedir() || process.env.USERPROFILE || process.env.HOME || '';
|
|
21
24
|
}
|
|
@@ -87,8 +90,10 @@ export function installBundledThemes(targetDir = themesTargetDir(), sourceDir =
|
|
|
87
90
|
try {
|
|
88
91
|
files = readdirSync(sourceDir).filter(entry => entry.toLowerCase().endsWith('.json'));
|
|
89
92
|
}
|
|
90
|
-
catch {
|
|
91
|
-
|
|
93
|
+
catch (error) {
|
|
94
|
+
// No file was attempted, so `failed` stays empty; the directory-level
|
|
95
|
+
// reason travels separately so the caller can log a precise line.
|
|
96
|
+
return { installed, skipped, repaired, failed, sourceError: `could not read bundled themes from ${sourceDir}: ${String(error)}` };
|
|
92
97
|
}
|
|
93
98
|
for (const file of files) {
|
|
94
99
|
const target = join(targetDir, file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tui-theme",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
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",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
61
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || 0.1.
|
|
62
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || 0.1.
|
|
61
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1",
|
|
63
63
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
@@ -240,3 +240,78 @@ assert.ok(followToast.text.includes('pink-day') && followToast.text.includes('re
|
|
|
240
240
|
assert.equal(JSON.parse(readFileSync(join(dataDir3, 'theme.json'), 'utf8')).theme, 'pink-day', 'the follow pref write still happened')
|
|
241
241
|
await mount3.fiber.dispose()
|
|
242
242
|
console.log('OK toast phase 3: apply-time self-heal and boot-follow toasts delivered on the real host')
|
|
243
|
+
|
|
244
|
+
// ── Phase 4: settings namespace registers against the REAL service, late ────
|
|
245
|
+
// The stub suites cover the namespace registration with fakes, and phase 3
|
|
246
|
+
// stands in a minimal fake at apply time; this phase mounts a real
|
|
247
|
+
// @deepseek-ai/dsh-settings SettingsProvider AFTER the plugin (and after the
|
|
248
|
+
// extensions row), so the plugin's parked ['settings'] inject fires against
|
|
249
|
+
// the genuine register()/scope/watch machinery. The end-to-end signal: a
|
|
250
|
+
// user-layer followSystem write through the real service flips theme.json.
|
|
251
|
+
const sandbox4 = mkdtempSync(join(tmpdir(), 'pink-settings-late-'))
|
|
252
|
+
process.env.USERPROFILE = sandbox4
|
|
253
|
+
process.env.HOME = sandbox4
|
|
254
|
+
const dataDir4 = join(sandbox4, '.dsh-tui')
|
|
255
|
+
mkdirSync(dataDir4, { recursive: true })
|
|
256
|
+
writeFileSync(join(dataDir4, 'theme-follow.json'), JSON.stringify({ light: true, at: 1 }, null, 2))
|
|
257
|
+
|
|
258
|
+
const { SettingsProvider } = await import(pathToFileURL(hostRequire.resolve('@deepseek-ai/dsh-settings')).href)
|
|
259
|
+
|
|
260
|
+
/** Minimal concrete provider: in-memory storage, no file, no background IO. */
|
|
261
|
+
class MemorySettingsProvider extends SettingsProvider {
|
|
262
|
+
static provide = 'settings'
|
|
263
|
+
writable = true
|
|
264
|
+
stored = {}
|
|
265
|
+
async load() {
|
|
266
|
+
return { ...this.stored }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async persist(ns, section) {
|
|
270
|
+
this.stored[ns] = { ...section }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const app4 = new Context()
|
|
275
|
+
await app4.plugin(pluginHost.default ?? pluginHost)
|
|
276
|
+
const mount4 = await mountPlugin(app4)
|
|
277
|
+
await mount4.context.plugin(pink)
|
|
278
|
+
await app4.plugin(extensions.default ?? extensions)
|
|
279
|
+
// Deliberately last: the namespace registration can only be "late" when the
|
|
280
|
+
// service arrives after everything the plugin injects at apply time.
|
|
281
|
+
await app4.plugin(MemorySettingsProvider)
|
|
282
|
+
|
|
283
|
+
const provider = app4.get('settings')
|
|
284
|
+
assert.ok(provider, 'the real dsh-settings provider must be mounted')
|
|
285
|
+
const settingsDeadline = Date.now() + 5_000
|
|
286
|
+
while (provider.get(SETTINGS_NAMESPACE) === undefined && Date.now() < settingsDeadline) {
|
|
287
|
+
await sleep(25)
|
|
288
|
+
}
|
|
289
|
+
assert.ok(
|
|
290
|
+
provider.get(SETTINGS_NAMESPACE),
|
|
291
|
+
`the plugin namespace must register with the real service within ${5_000}ms of its late arrival`,
|
|
292
|
+
)
|
|
293
|
+
await provider.update(SETTINGS_NAMESPACE, { followSystem: true })
|
|
294
|
+
assert.equal(
|
|
295
|
+
provider.get(SETTINGS_NAMESPACE)?.followSystem,
|
|
296
|
+
true,
|
|
297
|
+
'the real service must resolve the committed user layer for the namespace',
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
const prefPath4 = join(dataDir4, 'theme.json')
|
|
301
|
+
const followDeadline = Date.now() + 5_000
|
|
302
|
+
let followed = false
|
|
303
|
+
while (!followed && Date.now() < followDeadline) {
|
|
304
|
+
try {
|
|
305
|
+
followed = JSON.parse(readFileSync(prefPath4, 'utf8')).theme === 'pink-day'
|
|
306
|
+
} catch {
|
|
307
|
+
// Pref not written yet.
|
|
308
|
+
}
|
|
309
|
+
if (!followed) await sleep(25)
|
|
310
|
+
}
|
|
311
|
+
assert.equal(
|
|
312
|
+
followed,
|
|
313
|
+
true,
|
|
314
|
+
'the user-layer follow toggle must apply the cached light background through the real service',
|
|
315
|
+
)
|
|
316
|
+
await mount4.fiber.dispose()
|
|
317
|
+
console.log('OK settings: namespace registers late on the real dsh-settings service and drives the follow pref')
|
|
@@ -105,6 +105,13 @@ assert.deepEqual(
|
|
|
105
105
|
// guarantee, so the count is reported instead of pinned to a hardcoded floor.
|
|
106
106
|
console.log(`* host Theme key count: ${allKeys.length} (coverage asserted per key)`)
|
|
107
107
|
|
|
108
|
+
// The color keys the host's /settings screen paints with (screens/Settings.tsx
|
|
109
|
+
// at the pinned baseline: promptBorder card frame, permission card title,
|
|
110
|
+
// selectionBg focused row, suggestion/error/success/inactive/subtle row text
|
|
111
|
+
// and checkbox chips, warning badges). Maintained by hand because the screen
|
|
112
|
+
// has no exported key manifest; revisit this list whenever a dsh-TUI release
|
|
113
|
+
// adds a Theme key to that screen — the per-key coverage assertion below then
|
|
114
|
+
// fails loudly instead of shipping an unreadable settings card.
|
|
108
115
|
const settingsKeys = [
|
|
109
116
|
'promptBorder',
|
|
110
117
|
'selectionBg',
|
|
@@ -7,9 +7,14 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
import assert from 'node:assert/strict'
|
|
9
9
|
|
|
10
|
+
// This repo deliberately ships no package-lock.json: the pinned host tarball
|
|
11
|
+
// bundles @dsh-std/* packages whose manifests still declare workspace:*, and
|
|
12
|
+
// npm replays a committed lockfile faithfully — every install (npm ci too)
|
|
13
|
+
// dies with EUNSUPPORTEDPROTOCOL. Removing the lockfile makes npm skip the
|
|
14
|
+
// bundled directories instead. The peer/dev consistency checks below therefore
|
|
15
|
+
// read package.json only; CI's contract job guards the deletion (REVIEW.md G1).
|
|
10
16
|
const pluginRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
11
17
|
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
18
|
const packCommand = process.platform === 'win32'
|
|
14
19
|
? { file: 'cmd.exe', args: ['/d', '/s', '/c', 'npm pack --dry-run --json --ignore-scripts'] }
|
|
15
20
|
: { file: 'npm', args: ['pack', '--dry-run', '--json', '--ignore-scripts'] }
|
|
@@ -90,12 +95,10 @@ for (const name of Object.keys(packageJson.peerDependencies)) {
|
|
|
90
95
|
assert.equal(devAccepted, true, `${name} dev pin must be accepted by its peer range`)
|
|
91
96
|
}
|
|
92
97
|
assert.equal(packageJson.dependencies?.['@deepseek-ai/schemastery'], undefined)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
assert.deepEqual(rootLock.peerDependencies, packageJson.peerDependencies)
|
|
98
|
-
assert.equal(rootLock.dependencies, undefined)
|
|
98
|
+
// The lockfile's root metadata is deliberately not compared here: this script
|
|
99
|
+
// also ships inside the published tarball (package.json `files`), and npm never
|
|
100
|
+
// packs a lockfile, so the check could not hold for a consumer. The repo-level
|
|
101
|
+
// lockfile invariant lives in the CI contract job instead (REVIEW.md G1).
|
|
99
102
|
assert.equal(existsSync(new URL('../lib/types/index.js', import.meta.url)), true)
|
|
100
103
|
|
|
101
104
|
console.log(`✓ package manifest: ${packed.name}@${packed.version}, ${files.size} files`)
|
package/scripts/verify.mjs
CHANGED
|
@@ -650,6 +650,31 @@ const emit = (record, event, ...args) => {
|
|
|
650
650
|
console.log('✓ follow logging: baseline docs stay quiet, real toggles still log')
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
// ── 12c. unreadable bundled source: the directory-level failure stays precise ──
|
|
654
|
+
{
|
|
655
|
+
const targetDir = join(sandboxHome, 'source-fail-target')
|
|
656
|
+
const sourceDir = join(pluginRoot, 'themes')
|
|
657
|
+
const originalReaddir = builtinFs.readdirSync
|
|
658
|
+
try {
|
|
659
|
+
builtinFs.readdirSync = (path, ...rest) => {
|
|
660
|
+
if (String(path) === sourceDir) throw new Error('EACCES: source unreadable')
|
|
661
|
+
return originalReaddir(path, ...rest)
|
|
662
|
+
}
|
|
663
|
+
syncBuiltinESMExports()
|
|
664
|
+
const result = installBundledThemes(targetDir, sourceDir)
|
|
665
|
+
assert.deepEqual(result.failed, [], 'no per-file failure exists when nothing was attempted')
|
|
666
|
+
assert.match(
|
|
667
|
+
result.sourceError ?? '',
|
|
668
|
+
/could not read bundled themes from .*themes.*EACCES/,
|
|
669
|
+
'the directory-level error names the source and the cause',
|
|
670
|
+
)
|
|
671
|
+
} finally {
|
|
672
|
+
builtinFs.readdirSync = originalReaddir
|
|
673
|
+
syncBuiltinESMExports()
|
|
674
|
+
}
|
|
675
|
+
console.log('✓ unreadable bundled source: precise sourceError, no bogus per-file failure')
|
|
676
|
+
}
|
|
677
|
+
|
|
653
678
|
// ── 12. status injection: session handlers die with tuiStatus activation ────
|
|
654
679
|
{
|
|
655
680
|
const outerHandlers = new Map()
|