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 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
- `verify:host` 默认使用开发依赖中的 dsh-TUI(当前为 0.10.1)进行零配置验证;需要验证旧版或发布基线时,再显式指向同一版本的宿主 adapter 与源码。需要锁定版本时,额外设置 `DSH_TUI_EXPECTED_VERSION`。
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
 
@@ -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"}
@@ -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,CAiK7D"}
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"}
@@ -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;AAyGD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,GAAG,IAAI,CAgIvF"}
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"}
@@ -106,8 +106,7 @@ function activeThemeName(dataDir) {
106
106
  }
107
107
  return prefCache.value;
108
108
  }
109
- function isPinkThemeActive(dataDir) {
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(dataDir) {
155
- const name = activeThemeName(dataDir);
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 !== name || now - colorsCache.at >= THEME_PREF_TTL_MS) {
160
- colorsCache = { at: now, name, colors: readPalette(name) };
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 cells = statusCells(eff);
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
- const cells = statusCells(eff);
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(dataDir),
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
- function statusCells(eff) {
232
- const enabled = eff.statusEnabled && (eff.statusScope === 'all-themes' || isPinkThemeActive(dataDir));
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;CACnC;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;AAID,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,CAmCpB;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"}
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"}
@@ -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
- return { installed, skipped, repaired, failed: [sourceDir] };
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.0",
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.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 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.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 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",
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
- const rootLock = lockfile.packages?.['']
94
- assert.ok(rootLock, 'lockfile must have root metadata')
95
- assert.equal(lockfile.version, packageJson.version)
96
- assert.deepEqual(rootLock.devDependencies, packageJson.devDependencies)
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`)
@@ -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()