twskin 0.5.3

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/CONTRIBUTING.md +37 -0
  3. package/LICENSE +21 -0
  4. package/README.md +104 -0
  5. package/SECURITY.md +28 -0
  6. package/dist/bin/twskin.d.ts +3 -0
  7. package/dist/bin/twskin.d.ts.map +1 -0
  8. package/dist/bin/twskin.js +4 -0
  9. package/dist/bin/twskin.js.map +1 -0
  10. package/dist/commands.d.ts +41 -0
  11. package/dist/commands.d.ts.map +1 -0
  12. package/dist/commands.js +421 -0
  13. package/dist/commands.js.map +1 -0
  14. package/dist/context.d.ts +14 -0
  15. package/dist/context.d.ts.map +1 -0
  16. package/dist/context.js +51 -0
  17. package/dist/context.js.map +1 -0
  18. package/dist/errors.d.ts +9 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +20 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/lock.d.ts +3 -0
  23. package/dist/lock.d.ts.map +1 -0
  24. package/dist/lock.js +52 -0
  25. package/dist/lock.js.map +1 -0
  26. package/dist/main.d.ts +9 -0
  27. package/dist/main.d.ts.map +1 -0
  28. package/dist/main.js +92 -0
  29. package/dist/main.js.map +1 -0
  30. package/dist/output.d.ts +4 -0
  31. package/dist/output.d.ts.map +1 -0
  32. package/dist/output.js +27 -0
  33. package/dist/output.js.map +1 -0
  34. package/dist/persistent-cdp.d.ts +14 -0
  35. package/dist/persistent-cdp.d.ts.map +1 -0
  36. package/dist/persistent-cdp.js +140 -0
  37. package/dist/persistent-cdp.js.map +1 -0
  38. package/dist/system.d.ts +64 -0
  39. package/dist/system.d.ts.map +1 -0
  40. package/dist/system.js +234 -0
  41. package/dist/system.js.map +1 -0
  42. package/dist/theme-download.d.ts +9 -0
  43. package/dist/theme-download.d.ts.map +1 -0
  44. package/dist/theme-download.js +250 -0
  45. package/dist/theme-download.js.map +1 -0
  46. package/dist/themes.d.ts +9 -0
  47. package/dist/themes.d.ts.map +1 -0
  48. package/dist/themes.js +185 -0
  49. package/dist/themes.js.map +1 -0
  50. package/dist/types.d.ts +58 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +2 -0
  53. package/dist/types.js.map +1 -0
  54. package/docs/publishing.md +45 -0
  55. package/docs/theme-distribution.md +62 -0
  56. package/package.json +74 -0
  57. package/runtime/component-map.mjs +200 -0
  58. package/runtime/injector.mjs +705 -0
  59. package/runtime/manifest.json +47 -0
  60. package/runtime/restore.sh +33 -0
  61. package/runtime/skin.js +1133 -0
  62. package/runtime/start.sh +86 -0
  63. package/runtime/styles/base.css +687 -0
  64. package/runtime/styles/manager.css +346 -0
  65. package/runtime/token-map.mjs +432 -0
@@ -0,0 +1,45 @@
1
+ # Publishing `twskin`
2
+
3
+ The public npm package is unscoped: `twskin`.
4
+
5
+ ## First release
6
+
7
+ Run these commands locally after signing in to the public npm registry. Keep your npm password and one-time code private.
8
+
9
+ ```bash
10
+ npm login --registry=https://registry.npmjs.org
11
+ cd packages/cli
12
+ npm ci --ignore-scripts
13
+ npm run prepare:runtime
14
+ npm test
15
+ npm pack --dry-run
16
+ npm publish --access public
17
+ ```
18
+
19
+ This creates the current `twskin` version on npm. Verify it with:
20
+
21
+ ```bash
22
+ npm view twskin version --registry=https://registry.npmjs.org
23
+ npm install --global twskin
24
+ ```
25
+
26
+ ## Trusted publishing
27
+
28
+ After the first release, configure npm Trusted Publishing for `twskin`:
29
+
30
+ - GitHub owner: `Fullstop000`
31
+ - Repository: `trae-work-dream-skin`
32
+ - Workflow: `npm-publish.yml`
33
+ - Environment: `npm`
34
+ - Allowed action: `npm publish`
35
+
36
+ The workflow publishes automatically when a `v*` tag is pushed. It can also be run manually with an existing tag. The package version must match the tag without its `v` prefix.
37
+
38
+ Use the GitHub `npm` environment to require release approval. Do not add a long-lived npm publish token to repository secrets.
39
+
40
+ ## Later releases
41
+
42
+ 1. Update `packages/cli/package.json` and `package-lock.json` to the next version.
43
+ 2. Commit the version change and create a matching `v<version>` tag.
44
+ 3. Push the commit and tag. The npm workflow publishes the CLI.
45
+ 4. Create a GitHub Release from the same tag. The theme-release workflow publishes the matching theme pack.
@@ -0,0 +1,62 @@
1
+ # Theme distribution decision
2
+
3
+ ## Current decision
4
+
5
+ Themes are data, not CLI implementation. The `twskin` npm package
6
+ therefore contains no theme manifests or images.
7
+
8
+ The CLI loads themes from the user data directory:
9
+
10
+ ```text
11
+ ~/.trae-work-skin/themes/<theme-id>/
12
+ ```
13
+
14
+ `TWSKIN_THEMES_DIR` can override this location for development, managed desktops
15
+ or portable installations. The npm-installed CLI asks before downloading the
16
+ official theme pack on first start; the package itself contains no themes.
17
+
18
+ This boundary allows CLI and themes to have independent release cadences and
19
+ keeps large visual assets out of normal CLI upgrades.
20
+
21
+ ## Official GitHub Release protocol
22
+
23
+ Every CLI release attaches these independently downloadable assets:
24
+
25
+ ```text
26
+ twskin-themes.tar.gz
27
+ twskin-themes.sha256
28
+ twskin-themes-v<version>.tar.gz
29
+ twskin-themes-v<version>.sha256
30
+ ```
31
+
32
+ `twskin theme download [id]` downloads the fixed-name assets through GitHub's
33
+ `releases/latest/download` redirect, without calling the rate-limited Releases
34
+ API. The versioned copies remain available for archival and manual downloads.
35
+ With no ID, all themes are installed; with an ID, only that theme is installed.
36
+ The archive root contains `theme-pack.json` and `themes/<id>/` directories.
37
+
38
+ The command limits metadata and archive sizes, uses a network timeout, verifies
39
+ SHA-256 before extraction, rejects absolute/traversal paths and link/special file
40
+ types, validates every theme manifest and performs an atomic directory swap with
41
+ rollback. It never changes the active theme implicitly.
42
+
43
+ For testing or an enterprise mirror, `TWSKIN_RELEASE_ASSET_BASE_URL` may point
44
+ to a directory containing the fixed-name assets. The legacy
45
+ `TWSKIN_RELEASE_API_URL` override remains available for a GitHub-compatible
46
+ latest-release JSON endpoint.
47
+
48
+ When GitHub's anonymous API quota is unavailable, set `TWSKIN_GITHUB_TOKEN`,
49
+ `GH_TOKEN`, or `GITHUB_TOKEN`. The CLI only sends that token to HTTPS requests
50
+ for `api.github.com` and `github.com`; it is never forwarded to a configured
51
+ mirror.
52
+
53
+ ## Local loading
54
+
55
+ `twskin theme load <directory>` accepts either a directory containing `theme.json`
56
+ or a parent whose immediate children are theme directories. Only `theme.json`,
57
+ optional `theme.css`, canonical backgrounds and side-panel assets, and supported
58
+ files below `icons/` are copied. Symlinks and oversized themes are rejected.
59
+
60
+ Publishing a second npm package is not preferred: it would place mutable theme
61
+ data under `node_modules` and make selective updates awkward. Package signatures
62
+ or a signed catalog should be added before third-party registries are enabled.
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "twskin",
3
+ "version": "0.5.3",
4
+ "description": "An unofficial community theme manager and CLI for TRAE Work on macOS.",
5
+ "type": "module",
6
+ "bin": {
7
+ "twskin": "dist/bin/twskin.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/main.d.ts",
12
+ "import": "./dist/main.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist/",
17
+ "docs/",
18
+ "runtime/",
19
+ "CHANGELOG.md",
20
+ "CONTRIBUTING.md",
21
+ "LICENSE",
22
+ "README.md",
23
+ "SECURITY.md"
24
+ ],
25
+ "scripts": {
26
+ "prepare:runtime": "node ./scripts/prepare-runtime.mjs",
27
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
28
+ "typecheck": "tsc --noEmit",
29
+ "build": "npm run clean && tsc",
30
+ "check": "npm run typecheck && node ./scripts/check-package.mjs",
31
+ "test": "npm run build && node --test \"test/*.test.mjs\"",
32
+ "build:themes": "npm run build && node ../../scripts/build-theme-pack.mjs",
33
+ "prepack": "npm run prepare:runtime && npm run check && npm test",
34
+ "pack:dry-run": "npm pack --dry-run"
35
+ },
36
+ "engines": {
37
+ "node": ">=22.0.0"
38
+ },
39
+ "os": [
40
+ "darwin"
41
+ ],
42
+ "keywords": [
43
+ "trae",
44
+ "theme",
45
+ "theme-manager",
46
+ "cli",
47
+ "macos",
48
+ "electron",
49
+ "cdp"
50
+ ],
51
+ "author": "Fullstop000",
52
+ "license": "MIT",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/Fullstop000/trae-work-dream-skin.git",
56
+ "directory": "packages/cli"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/Fullstop000/trae-work-dream-skin/issues"
60
+ },
61
+ "homepage": "https://github.com/Fullstop000/trae-work-dream-skin#readme",
62
+ "publishConfig": {
63
+ "access": "public",
64
+ "registry": "https://registry.npmjs.org"
65
+ },
66
+ "dependencies": {
67
+ "@clack/prompts": "^1.7.0",
68
+ "jsonc-parser": "^3.3.1"
69
+ },
70
+ "devDependencies": {
71
+ "@types/node": "^22.20.1",
72
+ "typescript": "^7.0.2"
73
+ }
74
+ }
@@ -0,0 +1,200 @@
1
+ // TRAE Work Skin — V3 组件槽位规范化(纯函数,无 DOM 依赖)
2
+ // 页面载荷由 injector 嵌入 skin.js;单测可直接加载本文件。
3
+ // 前置条件:roles 必须是 TOKEN_MAP.deriveRoles 的完整输出。
4
+ (function (root, factory) {
5
+ const api = factory();
6
+ if (typeof module !== "undefined" && module.exports) module.exports = api;
7
+ root.DREAM_SKIN_COMPONENT_MAP = api;
8
+ })(typeof self !== "undefined" ? self : globalThis, function () {
9
+ "use strict";
10
+
11
+ const BANNER_VARIANTS = new Set(["default", "system-plate"]);
12
+ const MODE_TAB_VARIANTS = new Set(["default", "launch-rail"]);
13
+ const ICON_POLICIES = new Set(["active", "always", "never"]);
14
+ const MODES = ["work", "code", "design"];
15
+ const DEFAULT_BANNER_MODES = {
16
+ work: { code: "01", label: "WORK" },
17
+ code: { code: "02", label: "CODE" },
18
+ design: { code: "03", label: "DESIGN" },
19
+ };
20
+ const DEFAULT_TAB_MODES = {
21
+ work: { code: "01" },
22
+ code: { code: "02" },
23
+ design: { code: "03" },
24
+ };
25
+
26
+ const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
27
+ const number = (value, fallback, min, max) => {
28
+ const parsed = Number(value);
29
+ return Number.isFinite(parsed) ? Math.min(max, Math.max(min, parsed)) : fallback;
30
+ };
31
+ const choice = (value, values, fallback) => values.has(value) ? value : fallback;
32
+ const text = (value, fallback, maxLength = 32) => {
33
+ if (value == null || !["string", "number"].includes(typeof value)) return fallback;
34
+ const normalized = String(value)
35
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
36
+ .replace(/\s+/g, " ")
37
+ .trim();
38
+ return normalized ? normalized.slice(0, maxLength) : fallback;
39
+ };
40
+
41
+ // 主题输入只接受可审计的封闭颜色语法;fallback 来自 deriveRoles,属于可信内部值。
42
+ const isThemeColor = (value) => {
43
+ if (typeof value !== "string") return false;
44
+ const color = value.trim();
45
+ if (color.toLowerCase() === "transparent") return true;
46
+ if (/^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(color)) return true;
47
+ const match = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+))?\s*\)$/i.exec(color);
48
+ if (!match) return false;
49
+ const channels = match.slice(1, 4).map(Number);
50
+ const alpha = match[4] == null ? 1 : Number(match[4]);
51
+ return channels.every((channel) => Number.isFinite(channel) && channel >= 0 && channel <= 255)
52
+ && Number.isFinite(alpha) && alpha >= 0 && alpha <= 1;
53
+ };
54
+ const safeColor = (value, fallback) => isThemeColor(value) ? value.trim() : fallback;
55
+
56
+ const splitTopLevel = (value, separator) => {
57
+ const parts = [];
58
+ let depth = 0;
59
+ let start = 0;
60
+ for (let i = 0; i < value.length; i += 1) {
61
+ if (value[i] === "(") depth += 1;
62
+ else if (value[i] === ")") depth -= 1;
63
+ else if (value[i] === separator && depth === 0) {
64
+ parts.push(value.slice(start, i).trim());
65
+ start = i + 1;
66
+ }
67
+ if (depth < 0) return [];
68
+ }
69
+ if (depth !== 0) return [];
70
+ parts.push(value.slice(start).trim());
71
+ return parts;
72
+ };
73
+ const SHADOW_LENGTH = /^(?:0|[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em))$/i;
74
+ const isShadowLayer = (value) => {
75
+ let layer = value.trim();
76
+ if (!layer) return false;
77
+ if (/^inset\s+/i.test(layer)) layer = layer.replace(/^inset\s+/i, "");
78
+ let color = null;
79
+ const colorMatch = /(?:#[0-9a-f]{3}|#[0-9a-f]{6}|#[0-9a-f]{8}|rgba?\([^)]*\)|transparent)$/i.exec(layer);
80
+ if (colorMatch) {
81
+ color = colorMatch[0];
82
+ layer = layer.slice(0, colorMatch.index).trim();
83
+ }
84
+ if (color != null && !isThemeColor(color)) return false;
85
+ const lengths = layer.split(/\s+/);
86
+ return lengths.length >= 2 && lengths.length <= 4 && lengths.every((part) => SHADOW_LENGTH.test(part));
87
+ };
88
+ const safeShadow = (value, fallback = "none") => {
89
+ if (typeof value !== "string") return fallback;
90
+ const shadow = value.trim();
91
+ if (!shadow) return fallback;
92
+ if (shadow.toLowerCase() === "none") return "none";
93
+ if (/(?:url|var|attr|expression)\s*\(|[;{}]|\/\*|\*\//i.test(shadow)) return fallback;
94
+ const layers = splitTopLevel(shadow, ",");
95
+ return layers.length && layers.every(isShadowLayer) ? shadow : fallback;
96
+ };
97
+
98
+ const modeContent = (value, fallback, includeLabel) => {
99
+ const modes = object(value);
100
+ return Object.fromEntries(MODES.map((mode) => {
101
+ const source = object(modes[mode]);
102
+ const normalized = { code: text(source.code, fallback[mode].code, 24) };
103
+ if (includeLabel) normalized.label = text(source.label, fallback[mode].label, 24);
104
+ return [mode, normalized];
105
+ }));
106
+ };
107
+
108
+ function deriveComponents(components, roles) {
109
+ const c = object(components);
110
+ const r = object(roles);
111
+ const accent = object(r.accent);
112
+ const surface = object(r.surface);
113
+ const textRole = object(r.text);
114
+ const border = object(r.border);
115
+ const state = object(r.state);
116
+ const banner = object(object(c.landing).banner);
117
+ const bannerColors = object(banner.colors);
118
+ const bannerMetrics = object(banner.metrics);
119
+ const bannerEffects = object(banner.effects);
120
+ const bannerContent = object(banner.content);
121
+ const tabs = object(object(c.navigation).modeTabs);
122
+ const tabColors = object(tabs.colors);
123
+ const tabMetrics = object(tabs.metrics);
124
+ const tabEffects = object(tabs.effects);
125
+ const tabContent = object(tabs.content);
126
+
127
+ return {
128
+ banner: {
129
+ variant: choice(banner.variant, BANNER_VARIANTS, "default"),
130
+ colors: {
131
+ surface: safeColor(bannerColors.surface, surface.card),
132
+ border: safeColor(bannerColors.border, border.default),
133
+ accent: safeColor(bannerColors.accent, accent.base),
134
+ text: safeColor(bannerColors.text, textRole.primary),
135
+ muted: safeColor(bannerColors.muted, textRole.tertiary),
136
+ status: safeColor(bannerColors.status, state.success || accent.base),
137
+ },
138
+ metrics: {
139
+ cornerCutPx: number(bannerMetrics.cornerCutPx, 12, 0, 40),
140
+ maxWidthPx: number(bannerMetrics.maxWidthPx, 620, 320, 960),
141
+ iconSizePx: number(bannerMetrics.iconSizePx, 48, 24, 80),
142
+ },
143
+ effects: { shadow: safeShadow(bannerEffects.shadow, "none") },
144
+ content: {
145
+ statusText: text(bannerContent.statusText, "ONLINE", 20),
146
+ modes: modeContent(bannerContent.modes, DEFAULT_BANNER_MODES, true),
147
+ },
148
+ },
149
+ modeTabs: {
150
+ variant: choice(tabs.variant, MODE_TAB_VARIANTS, "default"),
151
+ colors: {
152
+ track: safeColor(tabColors.track, surface.secondary),
153
+ border: safeColor(tabColors.border, border.default),
154
+ indicator: safeColor(tabColors.indicator, accent.subtle || surface.card),
155
+ accent: safeColor(tabColors.accent, accent.base),
156
+ activeText: safeColor(tabColors.activeText, accent.base || textRole.primary),
157
+ inactiveText: safeColor(tabColors.inactiveText, textRole.secondary),
158
+ hover: safeColor(tabColors.hover, surface.cardHover || surface.tertiary),
159
+ },
160
+ metrics: {
161
+ heightPx: number(tabMetrics.heightPx, 40, 32, 56),
162
+ tabWidthPx: number(tabMetrics.tabWidthPx, 84, 64, 120),
163
+ gapPx: number(tabMetrics.gapPx, 3, 0, 12),
164
+ cornerCutPx: number(tabMetrics.cornerCutPx, 8, 0, 18),
165
+ },
166
+ effects: { shadow: safeShadow(tabEffects.shadow, "none") },
167
+ content: {
168
+ iconPolicy: choice(tabContent.iconPolicy, ICON_POLICIES, "active"),
169
+ showModeCode: tabContent.showModeCode === true,
170
+ modes: modeContent(tabContent.modes, DEFAULT_TAB_MODES, false),
171
+ },
172
+ },
173
+ };
174
+ }
175
+
176
+ function getContrastPairs(components) {
177
+ const c = object(components);
178
+ const banner = object(c.banner);
179
+ const bannerColors = object(banner.colors);
180
+ const modeTabs = object(c.modeTabs);
181
+ const tabColors = object(modeTabs.colors);
182
+ const pairs = [];
183
+ if (banner.variant === "system-plate") {
184
+ pairs.push(
185
+ { fg: "components.landing.banner.colors.text", fgValue: bannerColors.text, bg: "components.landing.banner.colors.surface", bgValue: bannerColors.surface, minRatio: 4 },
186
+ { fg: "components.landing.banner.colors.muted", fgValue: bannerColors.muted, bg: "components.landing.banner.colors.surface", bgValue: bannerColors.surface, minRatio: 3 },
187
+ { fg: "components.landing.banner.colors.status", fgValue: bannerColors.status, bg: "components.landing.banner.colors.surface", bgValue: bannerColors.surface, minRatio: 4 },
188
+ );
189
+ }
190
+ if (modeTabs.variant === "launch-rail") {
191
+ pairs.push(
192
+ { fg: "components.navigation.modeTabs.colors.activeText", fgValue: tabColors.activeText, bg: "components.navigation.modeTabs.colors.indicator", bgValue: tabColors.indicator, minRatio: 4 },
193
+ { fg: "components.navigation.modeTabs.colors.inactiveText", fgValue: tabColors.inactiveText, bg: "components.navigation.modeTabs.colors.track", bgValue: tabColors.track, minRatio: 4 },
194
+ );
195
+ }
196
+ return pairs;
197
+ }
198
+
199
+ return { deriveComponents, getContrastPairs, safeColor, safeShadow, MODES };
200
+ });