codex-terminal-themes 1.0.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.
Files changed (229) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CONTRIBUTING.md +23 -0
  3. package/LICENSE +21 -0
  4. package/LICENSE.txt +21 -0
  5. package/README.md +169 -0
  6. package/SECURITY.md +5 -0
  7. package/SUPPORT.md +5 -0
  8. package/bin/codex-terminal-themes.mjs +12 -0
  9. package/docs/app.js +1393 -0
  10. package/docs/assets/theme-gallery-header.png +0 -0
  11. package/docs/favicon.svg +10 -0
  12. package/docs/index.html +240 -0
  13. package/docs/preview.svg +24 -0
  14. package/docs/site-data.json +51078 -0
  15. package/docs/styles.css +780 -0
  16. package/metadata/README.md +71 -0
  17. package/metadata/themes.json +16185 -0
  18. package/metadata/themes.schema.json +218 -0
  19. package/package.json +109 -0
  20. package/src/cli.mjs +1763 -0
  21. package/stylelint.config.mjs +12 -0
  22. package/themes/80s.tmTheme +229 -0
  23. package/themes/Active4D.tmTheme +407 -0
  24. package/themes/All Hallow's Eve Custom.tmTheme +275 -0
  25. package/themes/All Hallow's Eve.tmTheme +277 -0
  26. package/themes/All Hallows Eve.tmTheme +277 -0
  27. package/themes/Amy.tmTheme +559 -0
  28. package/themes/Artic Fall.tmTheme +275 -0
  29. package/themes/BBEdit.tmTheme +439 -0
  30. package/themes/Bespin.tmTheme +516 -0
  31. package/themes/Black Pearl II.tmTheme +496 -0
  32. package/themes/Black Pearl.tmTheme +400 -0
  33. package/themes/Blackboard 2.tmTheme +348 -0
  34. package/themes/Blackboard Black.tmTheme +350 -0
  35. package/themes/Blackboard.tmTheme +348 -0
  36. package/themes/Blueberry Jelly.tmTheme +242 -0
  37. package/themes/Bluesy.tmTheme +242 -0
  38. package/themes/Blurb 2.tmTheme +229 -0
  39. package/themes/Blurb.tmTheme +229 -0
  40. package/themes/Bongzilla 2.tmTheme +223 -0
  41. package/themes/Bongzilla.tmTheme +223 -0
  42. package/themes/Brightly Dark.tmTheme +242 -0
  43. package/themes/Brilliance Black.tmTheme +2619 -0
  44. package/themes/Brilliance Dull.tmTheme +2243 -0
  45. package/themes/Bromozoid.tmTheme +227 -0
  46. package/themes/CSSEdit.tmTheme +203 -0
  47. package/themes/Classic Modified.tmTheme +469 -0
  48. package/themes/Clouds Midnight.tmTheme +361 -0
  49. package/themes/Clouds of Ruby.tmTheme +649 -0
  50. package/themes/Clouds.tmTheme +348 -0
  51. package/themes/Cloudy Fields.tmTheme +240 -0
  52. package/themes/Coal Graal.tmTheme +282 -0
  53. package/themes/Cobalt.tmTheme +561 -0
  54. package/themes/Coda.tmTheme +317 -0
  55. package/themes/Colorful.tmTheme +307 -0
  56. package/themes/Cool Glow.tmTheme +234 -0
  57. package/themes/Corona.tmTheme +290 -0
  58. package/themes/Cowabunga.tmTheme +242 -0
  59. package/themes/DanBurst.tmTheme +665 -0
  60. package/themes/Daniel Fischer.tmTheme +627 -0
  61. package/themes/Dark Ocean.tmTheme +242 -0
  62. package/themes/DarkNeon.tmTheme +818 -0
  63. package/themes/Dawn.tmTheme +437 -0
  64. package/themes/DawnCustom.tmTheme +443 -0
  65. package/themes/Django (Smoothy).tmTheme +453 -0
  66. package/themes/Django Blues.tmTheme +182 -0
  67. package/themes/Django Extended.tmTheme +495 -0
  68. package/themes/Django.tmTheme +436 -0
  69. package/themes/Dobdark.tmTheme +615 -0
  70. package/themes/Dominion Day.tmTheme +562 -0
  71. package/themes/Doo-Daa.tmTheme +242 -0
  72. package/themes/Drankin Purp.tmTheme +227 -0
  73. package/themes/Dreamweaver_Blackbam_Aptana303.tmTheme +980 -0
  74. package/themes/Easy on my Eyes There Buddy.tmTheme +227 -0
  75. package/themes/Eiffel.tmTheme +435 -0
  76. package/themes/Emacs Strict.tmTheme +241 -0
  77. package/themes/Emacs.tmTheme +241 -0
  78. package/themes/Epic Blue.tmTheme +320 -0
  79. package/themes/Erebus.tmTheme +467 -0
  80. package/themes/Espresso Libre.tmTheme +402 -0
  81. package/themes/Espresso Tutti.tmTheme +392 -0
  82. package/themes/Espresso.tmTheme +329 -0
  83. package/themes/Fade to Grey.tmTheme +308 -0
  84. package/themes/Fluidvision.tmTheme +443 -0
  85. package/themes/ForLaTeX.tmTheme +214 -0
  86. package/themes/Freckle.tmTheme +279 -0
  87. package/themes/Friendship Bracelet.tmTheme +303 -0
  88. package/themes/GaGaGaGroovy.tmTheme +227 -0
  89. package/themes/Gangrene.tmTheme +242 -0
  90. package/themes/GitHub.tmTheme +653 -0
  91. package/themes/GlitterBomb.tmTheme +387 -0
  92. package/themes/Halloween Night.tmTheme +303 -0
  93. package/themes/Happy happy joy joy.tmTheme +841 -0
  94. package/themes/Happydeluxe.tmTheme +184 -0
  95. package/themes/HelvectorLight.tmTheme +557 -0
  96. package/themes/Humane.tmTheme +220 -0
  97. package/themes/IDLE.tmTheme +235 -0
  98. package/themes/IR_Black.tmTheme +810 -0
  99. package/themes/IR_White.tmTheme +792 -0
  100. package/themes/Jane Fonda, Baby Redux.tmTheme +264 -0
  101. package/themes/Johnny.tmTheme +798 -0
  102. package/themes/Juicy.tmTheme +250 -0
  103. package/themes/Kuroir Theme.tmTheme +707 -0
  104. package/themes/LAZY.tmTheme +291 -0
  105. package/themes/Lowlight.tmTheme +605 -0
  106. package/themes/Mac Classic.tmTheme +476 -0
  107. package/themes/Made of Code.tmTheme +695 -0
  108. package/themes/MagicWB (Amiga).tmTheme +376 -0
  109. package/themes/Malibu Nights.tmTheme +257 -0
  110. package/themes/Menage A Trois.tmTheme +881 -0
  111. package/themes/Merbivore Soft.tmTheme +285 -0
  112. package/themes/Merbivore.tmTheme +285 -0
  113. package/themes/Midnight.tmTheme +321 -0
  114. package/themes/Mmm Sandy.tmTheme +227 -0
  115. package/themes/Monokai.tmTheme +289 -0
  116. package/themes/MultiMarkdown.tmTheme +183 -0
  117. package/themes/Mustang.tmTheme +339 -0
  118. package/themes/Neopro Inverted.tmTheme +328 -0
  119. package/themes/Neopro.tmTheme +330 -0
  120. package/themes/Nice One.tmTheme +222 -0
  121. package/themes/No Way.tmTheme +255 -0
  122. package/themes/Overcast.tmTheme +659 -0
  123. package/themes/Pastels on Dark.tmTheme +703 -0
  124. package/themes/Pastie.tmTheme +321 -0
  125. package/themes/Peridinkle.tmTheme +240 -0
  126. package/themes/Play!.tmTheme +736 -0
  127. package/themes/Puss.tmTheme +227 -0
  128. package/themes/Putty.tmTheme +275 -0
  129. package/themes/Quail.tmTheme +257 -0
  130. package/themes/RDark.tmTheme +235 -0
  131. package/themes/Rails Envy.tmTheme +299 -0
  132. package/themes/Railscasts 2.tmTheme +368 -0
  133. package/themes/Railscasts.tmTheme +278 -0
  134. package/themes/Resesif.tmTheme +298 -0
  135. package/themes/Ringo.tmTheme +240 -0
  136. package/themes/Ruby Blue.tmTheme +366 -0
  137. package/themes/RubyRobot.tmTheme +250 -0
  138. package/themes/Ryan Light.tmTheme +232 -0
  139. package/themes/Seafoam.tmTheme +242 -0
  140. package/themes/Sidewalk Chalk.tmTheme +276 -0
  141. package/themes/Sin City (that yellow bastard).tmTheme +585 -0
  142. package/themes/Slate.tmTheme +436 -0
  143. package/themes/Slush & Poppies.tmTheme +336 -0
  144. package/themes/Slush and Poppies.tmTheme +336 -0
  145. package/themes/Smokey Morning.tmTheme +229 -0
  146. package/themes/Smoothy original.tmTheme +623 -0
  147. package/themes/Smoothy.tmTheme +623 -0
  148. package/themes/Solarized (dark).tmTheme +2051 -0
  149. package/themes/Solarized-dark.tmTheme +312 -0
  150. package/themes/Solarized-light.tmTheme +305 -0
  151. package/themes/Sometheme.tmTheme +240 -0
  152. package/themes/SoylentTheme.tmTheme +353 -0
  153. package/themes/SpaceCadet.tmTheme +212 -0
  154. package/themes/Spectacular.tmTheme +436 -0
  155. package/themes/Starlight.tmTheme +857 -0
  156. package/themes/Stoneship Bright.tmTheme +348 -0
  157. package/themes/Stoneship.tmTheme +361 -0
  158. package/themes/Summer Camp Mod.tmTheme +229 -0
  159. package/themes/Summer Camp.tmTheme +229 -0
  160. package/themes/Summery Drink.tmTheme +242 -0
  161. package/themes/Sunburst.tmTheme +665 -0
  162. package/themes/Swyphs II.tmTheme +306 -0
  163. package/themes/Tango Bright.tmTheme +1 -0
  164. package/themes/Tango.tmTheme +450 -0
  165. package/themes/Teenage Dream.tmTheme +242 -0
  166. package/themes/Texari.tmTheme +727 -0
  167. package/themes/Text Ex Machina.tmTheme +295 -0
  168. package/themes/Tomorrow-Night-Blue.tmTheme +175 -0
  169. package/themes/Tomorrow-Night-Eighties.tmTheme +175 -0
  170. package/themes/Tomorrow-Night.tmTheme +175 -0
  171. package/themes/Tomorrow.tmTheme +349 -0
  172. package/themes/ToyChest.tmTheme +503 -0
  173. package/themes/TravisJeffery.tmTheme +1261 -0
  174. package/themes/Tubster.tmTheme +280 -0
  175. package/themes/Twilight BG FG.tmTheme +1000 -0
  176. package/themes/Twilight.tmTheme +516 -0
  177. package/themes/TwilightMod.tmTheme +529 -0
  178. package/themes/Two Days Ago.tmTheme +242 -0
  179. package/themes/Vibrant Fin.tmTheme +447 -0
  180. package/themes/Vibrant Ink.tmTheme +447 -0
  181. package/themes/Vibrant Scala.tmTheme +292 -0
  182. package/themes/Vibrant Tango.tmTheme +438 -0
  183. package/themes/Wandering.tmTheme +681 -0
  184. package/themes/Whimsy in Blue.tmTheme +240 -0
  185. package/themes/Why/342/200/231s Poignant.tmTheme" +191 -0
  186. package/themes/Windows XP.tmTheme +350 -0
  187. package/themes/Witch.tmTheme +282 -0
  188. package/themes/Yurple.tmTheme +227 -0
  189. package/themes/ZZZ.tmTheme +131 -0
  190. package/themes/Zachstronaut.tmTheme +381 -0
  191. package/themes/Zenburnesque.tmTheme +343 -0
  192. package/themes/[ Argonaut ].tmTheme +387 -0
  193. package/themes/barf.tmTheme +254 -0
  194. package/themes/converted-vscode-AmoledShinyBlack.tmTheme +528 -0
  195. package/themes/converted-vscode-AmoledShinyBlack2.tmTheme +609 -0
  196. package/themes/converted-vscode-AmoledShinyBlack3.tmTheme +683 -0
  197. package/themes/converted-vscode-AmoledShinyBlack4.tmTheme +896 -0
  198. package/themes/converted-vscode-AmoledShinyBlack5.tmTheme +1023 -0
  199. package/themes/converted-vscode-AmoledShinyBlack6.tmTheme +2092 -0
  200. package/themes/eclips3.media (ECLM).tmTheme +294 -0
  201. package/themes/evin.tmTheme +253 -0
  202. package/themes/fake.tmTheme +669 -0
  203. package/themes/fapfap.tmTheme +508 -0
  204. package/themes/helloKitty.tmTheme +293 -0
  205. package/themes/iLife 05.tmTheme +619 -0
  206. package/themes/iPlastic.tmTheme +397 -0
  207. package/themes/idleFingers.tmTheme +380 -0
  208. package/themes/krTheme.tmTheme +551 -0
  209. package/themes/mark.james.name.tmTheme +1117 -0
  210. package/themes/minimal Theme.tmTheme +551 -0
  211. package/themes/mint.tmTheme +617 -0
  212. package/themes/mintBlue Dark.tmTheme +653 -0
  213. package/themes/mintBlue.tmTheme +655 -0
  214. package/themes/modifiedPastels.tmTheme +745 -0
  215. package/themes/monoindustrial.tmTheme +451 -0
  216. package/themes/my-theme-blackboard.tmTheme +363 -0
  217. package/themes/my-theme-classic.tmTheme +465 -0
  218. package/themes/nppGLua.tmTheme +293 -0
  219. package/themes/reST testing theme.tmTheme +554 -0
  220. package/themes/rose-pine-dawn.tmTheme +329 -0
  221. package/themes/rose-pine-moon.tmTheme +329 -0
  222. package/themes/rose-pine.tmTheme +329 -0
  223. package/themes/ryan-light.tmTheme +232 -0
  224. package/themes/wut.tmTheme +255 -0
  225. package/tools/build-pages-site.mjs +465 -0
  226. package/tools/generate-theme-metadata.mjs +680 -0
  227. package/tools/serve-pages-site.mjs +196 -0
  228. package/tools/validate-themes.mjs +272 -0
  229. package/types/index.d.ts +49 -0
@@ -0,0 +1,196 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createServer } from "node:http";
3
+ import * as path from "node:path";
4
+
5
+ const defaultHost = "127.0.0.1";
6
+ const defaultPort = 4173;
7
+ const rootDirectory = process.cwd();
8
+ const docsDirectory = path.join(rootDirectory, "docs");
9
+
10
+ /**
11
+ * @param {string} docsRoot
12
+ *
13
+ * @returns {(
14
+ * request: import("node:http").IncomingMessage,
15
+ * response: import("node:http").ServerResponse
16
+ * ) => Promise<void>}
17
+ */
18
+ function createRequestHandler(docsRoot) {
19
+ return async (request, response) => {
20
+ const requestUrl = request.url ?? "/";
21
+ const filePath = getRequestedFilePath(docsRoot, requestUrl);
22
+
23
+ if (filePath === null) {
24
+ sendNotFound(response);
25
+ return;
26
+ }
27
+
28
+ await sendFile(response, filePath);
29
+ };
30
+ }
31
+
32
+ /**
33
+ * @param {string} filePath
34
+ *
35
+ * @returns {string}
36
+ */
37
+ function getContentType(filePath) {
38
+ const extension = path.extname(filePath).toLowerCase();
39
+
40
+ switch (extension) {
41
+ case ".css": {
42
+ return "text/css; charset=utf-8";
43
+ }
44
+
45
+ case ".html": {
46
+ return "text/html; charset=utf-8";
47
+ }
48
+
49
+ case ".js": {
50
+ return "text/javascript; charset=utf-8";
51
+ }
52
+
53
+ case ".json": {
54
+ return "application/json; charset=utf-8";
55
+ }
56
+
57
+ case ".svg": {
58
+ return "image/svg+xml";
59
+ }
60
+
61
+ default: {
62
+ return "application/octet-stream";
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param {string} optionName
69
+ * @param {number} fallback
70
+ *
71
+ * @returns {number}
72
+ */
73
+ function getIntegerOption(optionName, fallback) {
74
+ const value = getStringOption(optionName, "");
75
+ const parsedValue = Number.parseInt(value, 10);
76
+
77
+ return Number.isInteger(parsedValue) && parsedValue > 0
78
+ ? parsedValue
79
+ : fallback;
80
+ }
81
+
82
+ /**
83
+ * @param {string} docsRoot
84
+ * @param {string} requestUrl
85
+ *
86
+ * @returns {null | string}
87
+ */
88
+ function getRequestedFilePath(docsRoot, requestUrl) {
89
+ const parsedUrl = new URL(requestUrl, "http://localhost");
90
+ const decodedPath = decodeURIComponent(parsedUrl.pathname);
91
+ const relativePath =
92
+ decodedPath === "/" ? "index.html" : decodedPath.replace(/^\/+/v, "");
93
+ const filePath = path.resolve(docsRoot, relativePath);
94
+
95
+ return isPathInside(docsRoot, filePath) ? filePath : null;
96
+ }
97
+
98
+ /**
99
+ * @returns {{ host: string; port: number }}
100
+ */
101
+ function getServerOptions() {
102
+ return {
103
+ host: getStringOption("--host", defaultHost),
104
+ port: getIntegerOption("--port", defaultPort),
105
+ };
106
+ }
107
+
108
+ /**
109
+ * @param {string} optionName
110
+ * @param {string} fallback
111
+ *
112
+ * @returns {string}
113
+ */
114
+ function getStringOption(optionName, fallback) {
115
+ const prefix = `${optionName}=`;
116
+ const argument = process.argv.find((item) => item.startsWith(prefix));
117
+
118
+ return argument === undefined ? fallback : argument.slice(prefix.length);
119
+ }
120
+
121
+ /**
122
+ * @param {string} rootPath
123
+ * @param {string} candidatePath
124
+ *
125
+ * @returns {boolean}
126
+ */
127
+ function isPathInside(rootPath, candidatePath) {
128
+ const relativePath = path.relative(rootPath, candidatePath);
129
+
130
+ return (
131
+ relativePath.length === 0 ||
132
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
133
+ );
134
+ }
135
+
136
+ /**
137
+ * @returns {void}
138
+ */
139
+ function main() {
140
+ const { host, port } = getServerOptions();
141
+ const requestHandler = createRequestHandler(docsDirectory);
142
+ const server = createServer((request, response) => {
143
+ void requestHandler(request, response);
144
+ });
145
+
146
+ server.listen(port, host, () => {
147
+ process.stdout.write(
148
+ `Serving docs at http://${host}:${String(port)}/\n`
149
+ );
150
+ });
151
+ }
152
+
153
+ /**
154
+ * @returns {void}
155
+ */
156
+ function run() {
157
+ try {
158
+ main();
159
+ } catch (error) {
160
+ process.stderr.write(`${String(error)}\n`);
161
+ process.exitCode = 1;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * @param {import("node:http").ServerResponse} response
167
+ * @param {string} filePath
168
+ *
169
+ * @returns {Promise<void>}
170
+ */
171
+ async function sendFile(response, filePath) {
172
+ try {
173
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- The request path is constrained to the repo-local docs directory.
174
+ const contents = await readFile(filePath);
175
+ response.writeHead(200, {
176
+ "Content-Type": getContentType(filePath),
177
+ });
178
+ response.end(contents);
179
+ } catch {
180
+ sendNotFound(response);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * @param {import("node:http").ServerResponse} response
186
+ *
187
+ * @returns {void}
188
+ */
189
+ function sendNotFound(response) {
190
+ response.writeHead(404, {
191
+ "Content-Type": "text/plain; charset=utf-8",
192
+ });
193
+ response.end("Not found\n");
194
+ }
195
+
196
+ run();
@@ -0,0 +1,272 @@
1
+ import { XMLParser } from "fast-xml-parser";
2
+ import { SyntaxValidator } from "fast-xml-validator";
3
+ import { spawn } from "node:child_process";
4
+ import { once } from "node:events";
5
+ import { readdir, readFile, stat } from "node:fs/promises";
6
+ import * as path from "node:path";
7
+
8
+ const rootDirectory = process.cwd();
9
+ const themeDirectory = path.join(rootDirectory, "themes");
10
+ const shouldBuildBatCache = process.argv.includes("--bat-cache");
11
+
12
+ const parser = new XMLParser({
13
+ ignoreAttributes: false,
14
+ preserveOrder: true,
15
+ trimValues: false,
16
+ });
17
+
18
+ /**
19
+ * @typedef {{ filePath: string; ok: true; reason: "" }
20
+ * | { filePath: string; ok: false; reason: string }} ThemeValidationResult
21
+ */
22
+
23
+ /**
24
+ * @returns {Promise<number>}
25
+ */
26
+ async function buildBatCache() {
27
+ const childProcess = spawn("bat", ["cache", "--build"], {
28
+ stdio: "inherit",
29
+ });
30
+
31
+ const closeResult = /** @type {readonly unknown[]} */ (
32
+ await Promise.race([
33
+ once(childProcess, "close"),
34
+ once(childProcess, "error").then((errorResult) => {
35
+ const errorValues = /** @type {readonly unknown[]} */ (
36
+ errorResult
37
+ );
38
+ const error = errorValues[0];
39
+ throw error;
40
+ }),
41
+ ])
42
+ );
43
+ const [exitCode] = closeResult;
44
+
45
+ return typeof exitCode === "number" ? exitCode : 1;
46
+ }
47
+
48
+ /**
49
+ * @param {Record<string, unknown>} record
50
+ * @param {string} key
51
+ *
52
+ * @returns {readonly unknown[] | undefined}
53
+ */
54
+ function getArrayProperty(record, key) {
55
+ const value = record[key];
56
+ return isUnknownArray(value) ? value : undefined;
57
+ }
58
+
59
+ /**
60
+ * @param {unknown} value
61
+ *
62
+ * @returns {string | undefined}
63
+ */
64
+ function getTextNodeValue(value) {
65
+ if (!isUnknownArray(value)) {
66
+ return undefined;
67
+ }
68
+
69
+ const firstValue = value[0];
70
+ if (!isRecord(firstValue)) {
71
+ return undefined;
72
+ }
73
+
74
+ const textValue = firstValue["#text"];
75
+ return typeof textValue === "string" ? textValue : undefined;
76
+ }
77
+
78
+ /**
79
+ * @param {string} directory
80
+ *
81
+ * @returns {Promise<readonly string[]>}
82
+ */
83
+ async function getThemeFiles(directory) {
84
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- The validator intentionally reads the repo-local themes directory.
85
+ const directoryEntries = await readdir(directory, { withFileTypes: true });
86
+
87
+ return directoryEntries
88
+ .filter(
89
+ (directoryEntry) =>
90
+ directoryEntry.isFile() &&
91
+ directoryEntry.name.endsWith(".tmTheme")
92
+ )
93
+ .map((directoryEntry) => directoryEntry.name)
94
+ .toSorted((left, right) => left.localeCompare(right))
95
+ .map((fileName) => path.join(directory, fileName));
96
+ }
97
+
98
+ /**
99
+ * @param {unknown} parsedDocument
100
+ *
101
+ * @returns {readonly string[]}
102
+ */
103
+ function getTopLevelKeys(parsedDocument) {
104
+ if (!isUnknownArray(parsedDocument)) {
105
+ return [];
106
+ }
107
+
108
+ const plistNode = parsedDocument.find((node) =>
109
+ hasOwnRecordKey(node, "plist")
110
+ );
111
+
112
+ if (plistNode === undefined) {
113
+ return [];
114
+ }
115
+
116
+ const plistChildren = getArrayProperty(plistNode, "plist");
117
+ if (plistChildren === undefined) {
118
+ return [];
119
+ }
120
+
121
+ const dictNode = plistChildren.find((node) =>
122
+ hasOwnRecordKey(node, "dict")
123
+ );
124
+
125
+ if (dictNode === undefined) {
126
+ return [];
127
+ }
128
+
129
+ const dictChildren = getArrayProperty(dictNode, "dict");
130
+ if (dictChildren === undefined) {
131
+ return [];
132
+ }
133
+
134
+ return dictChildren.flatMap((node) => {
135
+ if (!hasOwnRecordKey(node, "key")) {
136
+ return [];
137
+ }
138
+
139
+ const keyName = getTextNodeValue(node.key);
140
+ return keyName === undefined ? [] : [keyName];
141
+ });
142
+ }
143
+
144
+ /**
145
+ * @param {unknown} value
146
+ * @param {string} key
147
+ *
148
+ * @returns {value is Record<string, unknown>}
149
+ */
150
+ function hasOwnRecordKey(value, key) {
151
+ return isRecord(value) && Object.hasOwn(value, key);
152
+ }
153
+
154
+ /**
155
+ * @param {unknown} value
156
+ *
157
+ * @returns {value is Record<string, unknown>}
158
+ */
159
+ function isRecord(value) {
160
+ return typeof value === "object" && value !== null && !Array.isArray(value);
161
+ }
162
+
163
+ /**
164
+ * @param {unknown} value
165
+ *
166
+ * @returns {value is readonly unknown[]}
167
+ */
168
+ function isUnknownArray(value) {
169
+ return Array.isArray(value);
170
+ }
171
+
172
+ /**
173
+ * @returns {Promise<number>}
174
+ */
175
+ async function main() {
176
+ const themeDirectoryStats = await stat(themeDirectory).catch(() => null);
177
+
178
+ if (themeDirectoryStats?.isDirectory() === false) {
179
+ process.stderr.write(
180
+ `Theme path is not a directory: ${themeDirectory}\n`
181
+ );
182
+ return 1;
183
+ }
184
+
185
+ if (themeDirectoryStats === null) {
186
+ process.stderr.write(
187
+ `Theme directory does not exist: ${themeDirectory}\n`
188
+ );
189
+ return 1;
190
+ }
191
+
192
+ const themeFiles = await getThemeFiles(themeDirectory);
193
+ const results = await Promise.all(
194
+ themeFiles.map((filePath) => validateTheme(filePath))
195
+ );
196
+ const failedResults = results.filter((result) => !result.ok);
197
+
198
+ if (failedResults.length > 0) {
199
+ for (const result of failedResults) {
200
+ process.stderr.write(
201
+ `${path.relative(rootDirectory, result.filePath)}: ${result.reason}\n`
202
+ );
203
+ }
204
+
205
+ return 1;
206
+ }
207
+
208
+ process.stdout.write(`Validated ${themeFiles.length} .tmTheme files.\n`);
209
+
210
+ if (shouldBuildBatCache) {
211
+ return buildBatCache();
212
+ }
213
+
214
+ return 0;
215
+ }
216
+
217
+ /**
218
+ * @returns {Promise<void>}
219
+ */
220
+ async function run() {
221
+ try {
222
+ process.exitCode = await main();
223
+ } catch (error) {
224
+ process.stderr.write(`${String(error)}\n`);
225
+ process.exitCode = 1;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * @param {string} filePath
231
+ *
232
+ * @returns {Promise<ThemeValidationResult>}
233
+ */
234
+ async function validateTheme(filePath) {
235
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Theme paths come from the repo-local themes directory listing.
236
+ const text = await readFile(filePath, "utf8");
237
+ const xmlValidation = SyntaxValidator.validate(text);
238
+
239
+ if (xmlValidation !== true) {
240
+ const { col, line, msg } = xmlValidation.err;
241
+ return {
242
+ filePath,
243
+ ok: false,
244
+ reason: `XML parse error at ${line}:${col}: ${msg}`,
245
+ };
246
+ }
247
+
248
+ const parsedDocument = /** @type {unknown} */ (parser.parse(text));
249
+ const keys = getTopLevelKeys(parsedDocument);
250
+ const missingKeys = [
251
+ "name",
252
+ "uuid",
253
+ "settings",
254
+ ].filter((key) => !keys.includes(key));
255
+
256
+ if (missingKeys.length > 0) {
257
+ return {
258
+ filePath,
259
+ ok: false,
260
+ reason: `Missing top-level key(s): ${missingKeys.join(", ")}`,
261
+ };
262
+ }
263
+
264
+ return {
265
+ filePath,
266
+ ok: true,
267
+ reason: "",
268
+ };
269
+ }
270
+
271
+ // eslint-disable-next-line unicorn/prefer-top-level-await -- This published-module config also enforces n/no-top-level-await.
272
+ void run();
@@ -0,0 +1,49 @@
1
+ export type ThemeAppearance = "dark" | "light" | "unknown";
2
+
3
+ export interface ThemeColors {
4
+ readonly background: null | string;
5
+ readonly caret: null | string;
6
+ readonly foreground: null | string;
7
+ readonly invisibles: null | string;
8
+ readonly lineHighlight: null | string;
9
+ readonly selection: null | string;
10
+ }
11
+
12
+ export interface ThemeMetadata {
13
+ readonly appearance: ThemeAppearance;
14
+ readonly author: null | string;
15
+ readonly colors: ThemeColors;
16
+ readonly colorSpace: null | string;
17
+ readonly fileName: string;
18
+ readonly id: string;
19
+ readonly name: string;
20
+ readonly path: string;
21
+ readonly scopes: readonly string[];
22
+ readonly semanticClass: null | string;
23
+ readonly statistics: ThemeStatistics;
24
+ readonly uuid: string;
25
+ }
26
+
27
+ export interface ThemeMetadataManifest {
28
+ readonly $schema: "./themes.schema.json";
29
+ readonly consumers: readonly string[];
30
+ readonly description: string;
31
+ readonly duplicateUuidGroups: Readonly<Record<string, readonly string[]>>;
32
+ readonly generatedBy: "npm run metadata:write";
33
+ readonly name: "codex-terminal-themes";
34
+ readonly schemaVersion: 1;
35
+ readonly themeCount: number;
36
+ readonly themeDirectory: "themes";
37
+ readonly themes: readonly ThemeMetadata[];
38
+ }
39
+
40
+ export interface ThemeStatistics {
41
+ readonly colorReferences: number;
42
+ readonly scopedSettings: number;
43
+ readonly settings: number;
44
+ readonly uniqueScopes: number;
45
+ }
46
+
47
+ declare const manifest: ThemeMetadataManifest;
48
+
49
+ export default manifest;