@wolfstar/cli 0.6.1 → 0.7.0-next-20260918190835

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 (36) hide show
  1. package/README.md +9 -0
  2. package/dist/{build-B0DNtlI8.js → build-BouwhG0y.js} +2 -2
  3. package/dist/{build-B0DNtlI8.js.map → build-BouwhG0y.js.map} +1 -1
  4. package/dist/{build-D4A4OlYR.js → build-CqVlp9y0.js} +2 -2
  5. package/dist/{build-D4A4OlYR.js.map → build-CqVlp9y0.js.map} +1 -1
  6. package/dist/cli.js +4 -4
  7. package/dist/{codegen-DHQH83M3.js → codegen-Do2h4Hl8.js} +2 -2
  8. package/dist/{codegen-DHQH83M3.js.map → codegen-Do2h4Hl8.js.map} +1 -1
  9. package/dist/{codegen-1kW_OnrL.js → codegen-y8gc147n.js} +2 -2
  10. package/dist/{codegen-1kW_OnrL.js.map → codegen-y8gc147n.js.map} +1 -1
  11. package/dist/{dev-CwJg6pSQ.js → dev-BgpW3Gsr.js} +9 -3
  12. package/dist/dev-BgpW3Gsr.js.map +1 -0
  13. package/dist/{dev-C-QKrdnz.js → dev-Dx9emKiU.js} +42 -8
  14. package/dist/dev-Dx9emKiU.js.map +1 -0
  15. package/dist/index.d.ts +42 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -1
  18. package/dist/{info-BrxKCcUW.js → info-Bvt1Jl88.js} +2 -2
  19. package/dist/{info-BrxKCcUW.js.map → info-Bvt1Jl88.js.map} +1 -1
  20. package/dist/{info-rYRLITnv.js → info-pX-M8R11.js} +2 -2
  21. package/dist/{info-rYRLITnv.js.map → info-pX-M8R11.js.map} +1 -1
  22. package/dist/{locales-DZCKWWcq.js → locales-C4_03V54.js} +2 -2
  23. package/dist/{locales-DZCKWWcq.js.map → locales-C4_03V54.js.map} +1 -1
  24. package/dist/theme-Bosd2Kk2.js +151 -0
  25. package/dist/theme-Bosd2Kk2.js.map +1 -0
  26. package/dist/tsc-D3FXjM6P.js +3 -0
  27. package/dist/{tsc-BTuFXw3O.js → tsc-D56BJ6lA.js} +8 -1
  28. package/dist/tsc-D56BJ6lA.js.map +1 -0
  29. package/dist/{tui-BCGQrMQo.js → tui-O94OH1w8.js} +151 -30
  30. package/dist/tui-O94OH1w8.js.map +1 -0
  31. package/package.json +2 -2
  32. package/dist/dev-C-QKrdnz.js.map +0 -1
  33. package/dist/dev-CwJg6pSQ.js.map +0 -1
  34. package/dist/tsc-BTuFXw3O.js.map +0 -1
  35. package/dist/tsc-Ba29w8eS.js +0 -3
  36. package/dist/tui-BCGQrMQo.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"locales-DZCKWWcq.js","names":[],"sources":["../src/lib/builders/index.ts","../src/lib/locales.ts"],"sourcesContent":["import type { ResolvedStarsConfig } from '@wolfstar/http-framework/config';\nimport { CliError } from '../errors.js';\nimport type { Builder } from './types.js';\n\n/**\n * Fails on experiments the CLI cannot honour yet, before a command gets far enough to look like it worked.\n *\n * Called by `stars dev`/`stars build` up front, since a project whose build tool needs no build at all would\n * otherwise return early and never reach {@link createBuilder}.\n */\nexport function assertSupportedExperiments(config: ResolvedStarsConfig): void {\n\tif (config.experimental.enableNitro) {\n\t\tthrow new CliError('`experimental.enableNitro` is not implemented yet', {\n\t\t\tcode: 'EXPERIMENT_UNAVAILABLE',\n\t\t\thint: \"Nitro needs the framework's Fetch adapter (wolfstar-project/stars-components#81); until it lands, use `build.tool` 'tsdown', 'vite' or 'tsc'.\"\n\t\t});\n\t}\n}\n\nexport async function createBuilder(config: ResolvedStarsConfig): Promise<Builder> {\n\tassertSupportedExperiments(config);\n\n\t// The project runs its own build, so the CLI only watches what it writes (see `ExternalBuilder`).\n\tif (config.experimental.enableExternalVite) {\n\t\tconst { ExternalBuilder } = await import('./external.js');\n\t\treturn new ExternalBuilder(config);\n\t}\n\n\tswitch (config.build.tool) {\n\t\tcase 'tsdown': {\n\t\t\tconst { TsdownBuilder } = await import('./tsdown.js');\n\t\t\treturn new TsdownBuilder(config);\n\t\t}\n\t\tcase 'vite': {\n\t\t\tconst { ViteBuilder } = await import('./vite.js');\n\t\t\treturn new ViteBuilder(config);\n\t\t}\n\t\tcase 'tsc': {\n\t\t\tconst { TscBuilder } = await import('./tsc.js');\n\t\t\treturn new TscBuilder(config);\n\t\t}\n\t\tcase 'none': {\n\t\t\tconst { NoneBuilder } = await import('./none.js');\n\t\t\treturn new NoneBuilder(config);\n\t\t}\n\t}\n}\n\nexport type { Builder, BuilderEvents, BuildOutcome } from './types.js';\n","import { EventEmitter } from 'node:events';\nimport { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs';\nimport { dirname, join, relative } from 'node:path';\nimport type { ResolvedStarsConfig } from '@wolfstar/http-framework/config';\nimport type { LogLevel } from './log-buffer.js';\n\nexport interface LocalesEvents {\n\tchange: [];\n\tlog: [level: LogLevel, text: string];\n}\n\n/** Keeps the conventional `src/locales` data directory next to the compiled application. */\nexport class Locales extends EventEmitter<LocalesEvents> {\n\treadonly source: string;\n\treadonly destination: string;\n\t#watcher: { close(): Promise<void> } | null = null;\n\n\tpublic constructor(private readonly config: ResolvedStarsConfig) {\n\t\tsuper();\n\t\tthis.source = join(config.root, 'src', 'locales');\n\t\tthis.destination = join(config.build.outDir, 'locales');\n\t}\n\n\tpublic copy(): boolean {\n\t\tif (this.config.build.tool === 'none' || !existsSync(this.source)) return false;\n\t\tmkdirSync(this.destination, { recursive: true });\n\t\tcpSync(this.source, this.destination, { recursive: true, force: true });\n\t\treturn true;\n\t}\n\n\tpublic async watch(): Promise<void> {\n\t\tif (this.config.build.tool === 'none' || !existsSync(this.source) || this.#watcher) return;\n\t\tconst { watch } = await import('chokidar');\n\t\tconst watcher = watch(this.source, { ignoreInitial: true });\n\t\tthis.#watcher = watcher;\n\n\t\twatcher.on('all', (event, path) => {\n\t\t\ttry {\n\t\t\t\tconst target = join(this.destination, relative(this.source, path));\n\t\t\t\tif (event === 'unlink' || event === 'unlinkDir') rmSync(target, { recursive: event === 'unlinkDir', force: true });\n\t\t\t\telse if (existsSync(path)) {\n\t\t\t\t\tmkdirSync(dirname(target), { recursive: true });\n\t\t\t\t\tcpSync(path, target, { recursive: event === 'addDir', force: true });\n\t\t\t\t}\n\t\t\t\tthis.emit('change');\n\t\t\t} catch (error) {\n\t\t\t\tthis.emit('log', 'error', `Failed to copy locales: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t}\n\t\t});\n\t\twatcher.on('error', (error) => this.emit('log', 'error', error instanceof Error ? error.message : String(error)));\n\t\tawait new Promise<void>((resolve) => watcher.once('ready', resolve));\n\t}\n\n\tpublic async close(): Promise<void> {\n\t\tconst watcher = this.#watcher;\n\t\tthis.#watcher = null;\n\t\tawait watcher?.close();\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AAUA,SAAgB,2BAA2B,QAAmC;CAC7E,IAAI,OAAO,aAAa,aACvB,MAAM,IAAI,SAAS,qDAAqD;EACvE,MAAM;EACN,MAAM;CACP,CAAC;AAEH;AAEA,eAAsB,cAAc,QAA+C;CAClF,2BAA2B,MAAM;CAGjC,IAAI,OAAO,aAAa,oBAAoB;EAC3C,MAAM,EAAE,oBAAoB,MAAM,OAAO;EACzC,OAAO,IAAI,gBAAgB,MAAM;CAClC;CAEA,QAAQ,OAAO,MAAM,MAArB;EACC,KAAK,UAAU;GACd,MAAM,EAAE,kBAAkB,MAAM,OAAO;GACvC,OAAO,IAAI,cAAc,MAAM;EAChC;EACA,KAAK,QAAQ;GACZ,MAAM,EAAE,gBAAgB,MAAM,OAAO;GACrC,OAAO,IAAI,YAAY,MAAM;EAC9B;EACA,KAAK,OAAO;GACX,MAAM,EAAE,eAAe,MAAM,OAAO;GACpC,OAAO,IAAI,WAAW,MAAM;EAC7B;EACA,KAAK,QAAQ;GACZ,MAAM,EAAE,gBAAgB,MAAM,OAAO;GACrC,OAAO,IAAI,YAAY,MAAM;EAC9B;CACD;AACD;;;;;AClCA,IAAa,UAAb,cAA6B,aAA4B;CAKpB;CAJpC,AAAS;CACT,AAAS;CACT,WAA8C;CAE9C,AAAO,YAAY,AAAiB,QAA6B;EAChE,MAAM;EAD6B;EAEnC,KAAK,SAAS,KAAK,OAAO,MAAM,OAAO,SAAS;EAChD,KAAK,cAAc,KAAK,OAAO,MAAM,QAAQ,SAAS;CACvD;CAEA,AAAO,OAAgB;EACtB,IAAI,KAAK,OAAO,MAAM,SAAS,UAAU,CAAC,WAAW,KAAK,MAAM,GAAG,OAAO;EAC1E,UAAU,KAAK,aAAa,EAAE,WAAW,KAAK,CAAC;EAC/C,OAAO,KAAK,QAAQ,KAAK,aAAa;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;EACtE,OAAO;CACR;CAEA,MAAa,QAAuB;EACnC,IAAI,KAAK,OAAO,MAAM,SAAS,UAAU,CAAC,WAAW,KAAK,MAAM,KAAK,KAAK,UAAU;EACpF,MAAM,EAAE,UAAU,MAAM,OAAO;EAC/B,MAAM,UAAU,MAAM,KAAK,QAAQ,EAAE,eAAe,KAAK,CAAC;EAC1D,KAAK,WAAW;EAEhB,QAAQ,GAAG,QAAQ,OAAO,SAAS;GAClC,IAAI;IACH,MAAM,SAAS,KAAK,KAAK,aAAa,SAAS,KAAK,QAAQ,IAAI,CAAC;IACjE,IAAI,UAAU,YAAY,UAAU,aAAa,OAAO,QAAQ;KAAE,WAAW,UAAU;KAAa,OAAO;IAAK,CAAC;SAC5G,IAAI,WAAW,IAAI,GAAG;KAC1B,UAAU,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;KAC9C,OAAO,MAAM,QAAQ;MAAE,WAAW,UAAU;MAAU,OAAO;KAAK,CAAC;IACpE;IACA,KAAK,KAAK,QAAQ;GACnB,SAAS,OAAO;IACf,KAAK,KAAK,OAAO,SAAS,2BAA2B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;GAC9G;EACD,CAAC;EACD,QAAQ,GAAG,UAAU,UAAU,KAAK,KAAK,OAAO,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC,CAAC;EAChH,MAAM,IAAI,SAAe,YAAY,QAAQ,KAAK,SAAS,OAAO,CAAC;CACpE;CAEA,MAAa,QAAuB;EACnC,MAAM,UAAU,KAAK;EACrB,KAAK,WAAW;EAChB,MAAM,SAAS,MAAM;CACtB;AACD"}
1
+ {"version":3,"file":"locales-C4_03V54.js","names":[],"sources":["../src/lib/builders/index.ts","../src/lib/locales.ts"],"sourcesContent":["import type { ResolvedStarsConfig } from '@wolfstar/http-framework/config';\nimport { CliError } from '../errors.js';\nimport type { Builder } from './types.js';\n\n/**\n * Fails on experiments the CLI cannot honour yet, before a command gets far enough to look like it worked.\n *\n * Called by `stars dev`/`stars build` up front, since a project whose build tool needs no build at all would\n * otherwise return early and never reach {@link createBuilder}.\n */\nexport function assertSupportedExperiments(config: ResolvedStarsConfig): void {\n\tif (config.experimental.enableNitro) {\n\t\tthrow new CliError('`experimental.enableNitro` is not implemented yet', {\n\t\t\tcode: 'EXPERIMENT_UNAVAILABLE',\n\t\t\thint: \"Nitro needs the framework's Fetch adapter (wolfstar-project/stars-components#81); until it lands, use `build.tool` 'tsdown', 'vite' or 'tsc'.\"\n\t\t});\n\t}\n}\n\nexport async function createBuilder(config: ResolvedStarsConfig): Promise<Builder> {\n\tassertSupportedExperiments(config);\n\n\t// The project runs its own build, so the CLI only watches what it writes (see `ExternalBuilder`).\n\tif (config.experimental.enableExternalVite) {\n\t\tconst { ExternalBuilder } = await import('./external.js');\n\t\treturn new ExternalBuilder(config);\n\t}\n\n\tswitch (config.build.tool) {\n\t\tcase 'tsdown': {\n\t\t\tconst { TsdownBuilder } = await import('./tsdown.js');\n\t\t\treturn new TsdownBuilder(config);\n\t\t}\n\t\tcase 'vite': {\n\t\t\tconst { ViteBuilder } = await import('./vite.js');\n\t\t\treturn new ViteBuilder(config);\n\t\t}\n\t\tcase 'tsc': {\n\t\t\tconst { TscBuilder } = await import('./tsc.js');\n\t\t\treturn new TscBuilder(config);\n\t\t}\n\t\tcase 'none': {\n\t\t\tconst { NoneBuilder } = await import('./none.js');\n\t\t\treturn new NoneBuilder(config);\n\t\t}\n\t}\n}\n\nexport type { Builder, BuilderEvents, BuildOutcome } from './types.js';\n","import { EventEmitter } from 'node:events';\nimport { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs';\nimport { dirname, join, relative } from 'node:path';\nimport type { ResolvedStarsConfig } from '@wolfstar/http-framework/config';\nimport type { LogLevel } from './log-buffer.js';\n\nexport interface LocalesEvents {\n\tchange: [];\n\tlog: [level: LogLevel, text: string];\n}\n\n/** Keeps the conventional `src/locales` data directory next to the compiled application. */\nexport class Locales extends EventEmitter<LocalesEvents> {\n\treadonly source: string;\n\treadonly destination: string;\n\t#watcher: { close(): Promise<void> } | null = null;\n\n\tpublic constructor(private readonly config: ResolvedStarsConfig) {\n\t\tsuper();\n\t\tthis.source = join(config.root, 'src', 'locales');\n\t\tthis.destination = join(config.build.outDir, 'locales');\n\t}\n\n\tpublic copy(): boolean {\n\t\tif (this.config.build.tool === 'none' || !existsSync(this.source)) return false;\n\t\tmkdirSync(this.destination, { recursive: true });\n\t\tcpSync(this.source, this.destination, { recursive: true, force: true });\n\t\treturn true;\n\t}\n\n\tpublic async watch(): Promise<void> {\n\t\tif (this.config.build.tool === 'none' || !existsSync(this.source) || this.#watcher) return;\n\t\tconst { watch } = await import('chokidar');\n\t\tconst watcher = watch(this.source, { ignoreInitial: true });\n\t\tthis.#watcher = watcher;\n\n\t\twatcher.on('all', (event, path) => {\n\t\t\ttry {\n\t\t\t\tconst target = join(this.destination, relative(this.source, path));\n\t\t\t\tif (event === 'unlink' || event === 'unlinkDir') rmSync(target, { recursive: event === 'unlinkDir', force: true });\n\t\t\t\telse if (existsSync(path)) {\n\t\t\t\t\tmkdirSync(dirname(target), { recursive: true });\n\t\t\t\t\tcpSync(path, target, { recursive: event === 'addDir', force: true });\n\t\t\t\t}\n\t\t\t\tthis.emit('change');\n\t\t\t} catch (error) {\n\t\t\t\tthis.emit('log', 'error', `Failed to copy locales: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t}\n\t\t});\n\t\twatcher.on('error', (error) => this.emit('log', 'error', error instanceof Error ? error.message : String(error)));\n\t\tawait new Promise<void>((resolve) => watcher.once('ready', resolve));\n\t}\n\n\tpublic async close(): Promise<void> {\n\t\tconst watcher = this.#watcher;\n\t\tthis.#watcher = null;\n\t\tawait watcher?.close();\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AAUA,SAAgB,2BAA2B,QAAmC;CAC7E,IAAI,OAAO,aAAa,aACvB,MAAM,IAAI,SAAS,qDAAqD;EACvE,MAAM;EACN,MAAM;CACP,CAAC;AAEH;AAEA,eAAsB,cAAc,QAA+C;CAClF,2BAA2B,MAAM;CAGjC,IAAI,OAAO,aAAa,oBAAoB;EAC3C,MAAM,EAAE,oBAAoB,MAAM,OAAO;EACzC,OAAO,IAAI,gBAAgB,MAAM;CAClC;CAEA,QAAQ,OAAO,MAAM,MAArB;EACC,KAAK,UAAU;GACd,MAAM,EAAE,kBAAkB,MAAM,OAAO;GACvC,OAAO,IAAI,cAAc,MAAM;EAChC;EACA,KAAK,QAAQ;GACZ,MAAM,EAAE,gBAAgB,MAAM,OAAO;GACrC,OAAO,IAAI,YAAY,MAAM;EAC9B;EACA,KAAK,OAAO;GACX,MAAM,EAAE,eAAe,MAAM,OAAO;GACpC,OAAO,IAAI,WAAW,MAAM;EAC7B;EACA,KAAK,QAAQ;GACZ,MAAM,EAAE,gBAAgB,MAAM,OAAO;GACrC,OAAO,IAAI,YAAY,MAAM;EAC9B;CACD;AACD;;;;;AClCA,IAAa,UAAb,cAA6B,aAA4B;CAKpB;CAJpC,AAAS;CACT,AAAS;CACT,WAA8C;CAE9C,AAAO,YAAY,AAAiB,QAA6B;EAChE,MAAM;EAD6B;EAEnC,KAAK,SAAS,KAAK,OAAO,MAAM,OAAO,SAAS;EAChD,KAAK,cAAc,KAAK,OAAO,MAAM,QAAQ,SAAS;CACvD;CAEA,AAAO,OAAgB;EACtB,IAAI,KAAK,OAAO,MAAM,SAAS,UAAU,CAAC,WAAW,KAAK,MAAM,GAAG,OAAO;EAC1E,UAAU,KAAK,aAAa,EAAE,WAAW,KAAK,CAAC;EAC/C,OAAO,KAAK,QAAQ,KAAK,aAAa;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;EACtE,OAAO;CACR;CAEA,MAAa,QAAuB;EACnC,IAAI,KAAK,OAAO,MAAM,SAAS,UAAU,CAAC,WAAW,KAAK,MAAM,KAAK,KAAK,UAAU;EACpF,MAAM,EAAE,UAAU,MAAM,OAAO;EAC/B,MAAM,UAAU,MAAM,KAAK,QAAQ,EAAE,eAAe,KAAK,CAAC;EAC1D,KAAK,WAAW;EAEhB,QAAQ,GAAG,QAAQ,OAAO,SAAS;GAClC,IAAI;IACH,MAAM,SAAS,KAAK,KAAK,aAAa,SAAS,KAAK,QAAQ,IAAI,CAAC;IACjE,IAAI,UAAU,YAAY,UAAU,aAAa,OAAO,QAAQ;KAAE,WAAW,UAAU;KAAa,OAAO;IAAK,CAAC;SAC5G,IAAI,WAAW,IAAI,GAAG;KAC1B,UAAU,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;KAC9C,OAAO,MAAM,QAAQ;MAAE,WAAW,UAAU;MAAU,OAAO;KAAK,CAAC;IACpE;IACA,KAAK,KAAK,QAAQ;GACnB,SAAS,OAAO;IACf,KAAK,KAAK,OAAO,SAAS,2BAA2B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;GAC9G;EACD,CAAC;EACD,QAAQ,GAAG,UAAU,UAAU,KAAK,KAAK,OAAO,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC,CAAC;EAChH,MAAM,IAAI,SAAe,YAAY,QAAQ,KAAK,SAAS,OAAO,CAAC;CACpE;CAEA,MAAa,QAAuB;EACnC,MAAM,UAAU,KAAK;EACrB,KAAK,WAAW;EAChB,MAAM,SAAS,MAAM;CACtB;AACD"}
@@ -0,0 +1,151 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ //#region src/lib/theme.ts
6
+ /** Every theme a user can pick, in the order the picker lists them. `auto` follows the terminal background. */
7
+ const THEME_SETTINGS = [
8
+ "auto",
9
+ "dark",
10
+ "light",
11
+ "dark-daltonized",
12
+ "light-daltonized",
13
+ "dark-ansi",
14
+ "light-ansi"
15
+ ];
16
+ const THEMES = {
17
+ dark: {
18
+ brand: "#4ade80",
19
+ url: "#22d3ee",
20
+ tunnel: "#e879f9",
21
+ busy: "#facc15",
22
+ success: "#4ade80",
23
+ warning: "#facc15",
24
+ error: "#f87171",
25
+ text: void 0,
26
+ onBright: "#000000",
27
+ onError: "#000000"
28
+ },
29
+ light: {
30
+ brand: "#15803d",
31
+ url: "#0e7490",
32
+ tunnel: "#a21caf",
33
+ busy: "#a16207",
34
+ success: "#15803d",
35
+ warning: "#a16207",
36
+ error: "#b91c1c",
37
+ text: void 0,
38
+ onBright: "#ffffff",
39
+ onError: "#ffffff"
40
+ },
41
+ "dark-daltonized": {
42
+ brand: "#56b4e9",
43
+ url: "#56b4e9",
44
+ tunnel: "#cc79a7",
45
+ busy: "#f0e442",
46
+ success: "#56b4e9",
47
+ warning: "#e69f00",
48
+ error: "#ff8a3d",
49
+ text: void 0,
50
+ onBright: "#000000",
51
+ onError: "#000000"
52
+ },
53
+ "light-daltonized": {
54
+ brand: "#0072b2",
55
+ url: "#0072b2",
56
+ tunnel: "#8e3b76",
57
+ busy: "#8a6500",
58
+ success: "#0072b2",
59
+ warning: "#8a6500",
60
+ error: "#b84a00",
61
+ text: void 0,
62
+ onBright: "#ffffff",
63
+ onError: "#ffffff"
64
+ },
65
+ "dark-ansi": {
66
+ brand: "greenBright",
67
+ url: "cyanBright",
68
+ tunnel: "magentaBright",
69
+ busy: "yellowBright",
70
+ success: "greenBright",
71
+ warning: "yellowBright",
72
+ error: "redBright",
73
+ text: void 0,
74
+ onBright: "black",
75
+ onError: "black"
76
+ },
77
+ "light-ansi": {
78
+ brand: "green",
79
+ url: "cyan",
80
+ tunnel: "magenta",
81
+ busy: "yellow",
82
+ success: "green",
83
+ warning: "yellow",
84
+ error: "red",
85
+ text: void 0,
86
+ onBright: "white",
87
+ onError: "white"
88
+ }
89
+ };
90
+ function isThemeSetting(value) {
91
+ return typeof value === "string" && THEME_SETTINGS.includes(value);
92
+ }
93
+ /**
94
+ * Guesses whether the terminal background is light from `COLORFGBG` (`foreground;background`, set by rxvt, Konsole and
95
+ * others). Without it the terminal is assumed dark, which matches most developer setups.
96
+ */
97
+ function detectTerminalBackground(env = process.env) {
98
+ const background = env.COLORFGBG?.split(";").at(-1);
99
+ if (background === void 0 || !/^\d+$/.test(background)) return "dark";
100
+ const index = Number(background);
101
+ return index === 7 || index === 15 ? "light" : "dark";
102
+ }
103
+ function resolveTheme(setting, env = process.env) {
104
+ return setting === "auto" ? detectTerminalBackground(env) : setting;
105
+ }
106
+ /**
107
+ * Precedence: `--theme` › `STARS_THEME` › the saved preference › `auto`. An unknown `STARS_THEME` is ignored, since an
108
+ * environment variable should not stop the dev server; an unknown `--theme` is the caller's to reject.
109
+ */
110
+ function resolveThemeSetting({ flag, env = process.env, saved }) {
111
+ if (isThemeSetting(flag)) return flag;
112
+ const fromEnv = env.STARS_THEME?.trim().toLowerCase();
113
+ if (isThemeSetting(fromEnv)) return fromEnv;
114
+ return saved ?? "auto";
115
+ }
116
+ /** Where preferences live: `$STARS_CONFIG_DIR`, else `$XDG_CONFIG_HOME/stars`, `%APPDATA%\stars` or `~/.config/stars`. */
117
+ function preferencesPath(env = process.env) {
118
+ const directory = env.STARS_CONFIG_DIR ?? join((env.XDG_CONFIG_HOME || (process.platform === "win32" ? env.APPDATA : void 0)) ?? join(homedir(), ".config"), "stars");
119
+ return join(directory, "preferences.json");
120
+ }
121
+ function readSavedTheme(env = process.env) {
122
+ try {
123
+ const { theme } = JSON.parse(readFileSync(preferencesPath(env), "utf8"));
124
+ return isThemeSetting(theme) ? theme : void 0;
125
+ } catch {
126
+ return;
127
+ }
128
+ }
129
+ /** Persists the theme, keeping any other preference in the file. Returns whether it could be written. */
130
+ function saveTheme(setting, env = process.env) {
131
+ const path = preferencesPath(env);
132
+ try {
133
+ let existing = {};
134
+ try {
135
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
136
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
137
+ } catch {}
138
+ mkdirSync(dirname(path), { recursive: true });
139
+ writeFileSync(path, `${JSON.stringify({
140
+ ...existing,
141
+ theme: setting
142
+ }, null, " ")}\n`);
143
+ return true;
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+
149
+ //#endregion
150
+ export { readSavedTheme as a, saveTheme as c, isThemeSetting as i, THEME_SETTINGS as n, resolveTheme as o, detectTerminalBackground as r, resolveThemeSetting as s, THEMES as t };
151
+ //# sourceMappingURL=theme-Bosd2Kk2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-Bosd2Kk2.js","names":[],"sources":["../src/lib/theme.ts"],"sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n/** Every theme a user can pick, in the order the picker lists them. `auto` follows the terminal background. */\nexport const THEME_SETTINGS = ['auto', 'dark', 'light', 'dark-daltonized', 'light-daltonized', 'dark-ansi', 'light-ansi'] as const;\nexport type ThemeSetting = (typeof THEME_SETTINGS)[number];\nexport type ThemeName = Exclude<ThemeSetting, 'auto'>;\n\n/**\n * Semantic colour roles the TUI paints with, instead of hardcoded colour names. `undefined` leaves the terminal's own\n * foreground colour, which is the only legible choice for body text on both dark and light backgrounds.\n */\nexport interface Theme {\n\t/** The wordmark, progress bar and selection marker. */\n\tbrand: string;\n\turl: string;\n\ttunnel: string;\n\t/** Something is still building or starting. */\n\tbusy: string;\n\tsuccess: string;\n\twarning: string;\n\terror: string;\n\ttext: string | undefined;\n\t/** Foreground of a badge painted on `success`, `warning` or `busy`. */\n\tonBright: string;\n\t/** Foreground of a badge painted on `error`. */\n\tonError: string;\n}\n\nexport type ThemeToken = keyof Theme;\n\nexport const THEMES: Readonly<Record<ThemeName, Theme>> = {\n\tdark: {\n\t\tbrand: '#4ade80',\n\t\turl: '#22d3ee',\n\t\ttunnel: '#e879f9',\n\t\tbusy: '#facc15',\n\t\tsuccess: '#4ade80',\n\t\twarning: '#facc15',\n\t\terror: '#f87171',\n\t\ttext: undefined,\n\t\tonBright: '#000000',\n\t\tonError: '#000000'\n\t},\n\tlight: {\n\t\tbrand: '#15803d',\n\t\turl: '#0e7490',\n\t\ttunnel: '#a21caf',\n\t\tbusy: '#a16207',\n\t\tsuccess: '#15803d',\n\t\twarning: '#a16207',\n\t\terror: '#b91c1c',\n\t\ttext: undefined,\n\t\tonBright: '#ffffff',\n\t\tonError: '#ffffff'\n\t},\n\t// Okabe-Ito palette: blue/orange carry the good/bad distinction instead of green/red.\n\t'dark-daltonized': {\n\t\tbrand: '#56b4e9',\n\t\turl: '#56b4e9',\n\t\ttunnel: '#cc79a7',\n\t\tbusy: '#f0e442',\n\t\tsuccess: '#56b4e9',\n\t\twarning: '#e69f00',\n\t\terror: '#ff8a3d',\n\t\ttext: undefined,\n\t\tonBright: '#000000',\n\t\tonError: '#000000'\n\t},\n\t'light-daltonized': {\n\t\tbrand: '#0072b2',\n\t\turl: '#0072b2',\n\t\ttunnel: '#8e3b76',\n\t\tbusy: '#8a6500',\n\t\tsuccess: '#0072b2',\n\t\twarning: '#8a6500',\n\t\terror: '#b84a00',\n\t\ttext: undefined,\n\t\tonBright: '#ffffff',\n\t\tonError: '#ffffff'\n\t},\n\t// The 16 ANSI colours only, so the terminal's own palette decides what they look like.\n\t'dark-ansi': {\n\t\tbrand: 'greenBright',\n\t\turl: 'cyanBright',\n\t\ttunnel: 'magentaBright',\n\t\tbusy: 'yellowBright',\n\t\tsuccess: 'greenBright',\n\t\twarning: 'yellowBright',\n\t\terror: 'redBright',\n\t\ttext: undefined,\n\t\tonBright: 'black',\n\t\tonError: 'black'\n\t},\n\t'light-ansi': {\n\t\tbrand: 'green',\n\t\turl: 'cyan',\n\t\ttunnel: 'magenta',\n\t\tbusy: 'yellow',\n\t\tsuccess: 'green',\n\t\twarning: 'yellow',\n\t\terror: 'red',\n\t\ttext: undefined,\n\t\tonBright: 'white',\n\t\tonError: 'white'\n\t}\n};\n\nexport function isThemeSetting(value: unknown): value is ThemeSetting {\n\treturn typeof value === 'string' && (THEME_SETTINGS as readonly string[]).includes(value);\n}\n\n/**\n * Guesses whether the terminal background is light from `COLORFGBG` (`foreground;background`, set by rxvt, Konsole and\n * others). Without it the terminal is assumed dark, which matches most developer setups.\n */\nexport function detectTerminalBackground(env: NodeJS.ProcessEnv = process.env): 'dark' | 'light' {\n\tconst background = env.COLORFGBG?.split(';').at(-1);\n\tif (background === undefined || !/^\\d+$/.test(background)) return 'dark';\n\tconst index = Number(background);\n\treturn index === 7 || index === 15 ? 'light' : 'dark';\n}\n\nexport function resolveTheme(setting: ThemeSetting, env: NodeJS.ProcessEnv = process.env): ThemeName {\n\treturn setting === 'auto' ? detectTerminalBackground(env) : setting;\n}\n\nexport interface ThemeSettingSources {\n\t/** `--theme`. */\n\tflag?: string;\n\tenv?: NodeJS.ProcessEnv;\n\t/** The theme saved by the picker. */\n\tsaved?: ThemeSetting;\n}\n\n/**\n * Precedence: `--theme` › `STARS_THEME` › the saved preference › `auto`. An unknown `STARS_THEME` is ignored, since an\n * environment variable should not stop the dev server; an unknown `--theme` is the caller's to reject.\n */\nexport function resolveThemeSetting({ flag, env = process.env, saved }: ThemeSettingSources): ThemeSetting {\n\tif (isThemeSetting(flag)) return flag;\n\tconst fromEnv = env.STARS_THEME?.trim().toLowerCase();\n\tif (isThemeSetting(fromEnv)) return fromEnv;\n\treturn saved ?? 'auto';\n}\n\n/** Where preferences live: `$STARS_CONFIG_DIR`, else `$XDG_CONFIG_HOME/stars`, `%APPDATA%\\stars` or `~/.config/stars`. */\nexport function preferencesPath(env: NodeJS.ProcessEnv = process.env): string {\n\tconst directory =\n\t\tenv.STARS_CONFIG_DIR ??\n\t\tjoin((env.XDG_CONFIG_HOME || (process.platform === 'win32' ? env.APPDATA : undefined)) ?? join(homedir(), '.config'), 'stars');\n\treturn join(directory, 'preferences.json');\n}\n\nexport function readSavedTheme(env: NodeJS.ProcessEnv = process.env): ThemeSetting | undefined {\n\ttry {\n\t\tconst { theme } = JSON.parse(readFileSync(preferencesPath(env), 'utf8')) as { theme?: unknown };\n\t\treturn isThemeSetting(theme) ? theme : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Persists the theme, keeping any other preference in the file. Returns whether it could be written. */\nexport function saveTheme(setting: ThemeSetting, env: NodeJS.ProcessEnv = process.env): boolean {\n\tconst path = preferencesPath(env);\n\ttry {\n\t\tlet existing: Record<string, unknown> = {};\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));\n\t\t\tif (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) existing = parsed as Record<string, unknown>;\n\t\t} catch {\n\t\t\t// No file yet, or a corrupt one: start over.\n\t\t}\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, `${JSON.stringify({ ...existing, theme: setting }, null, '\\t')}\\n`);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n"],"mappings":";;;;;;AAKA,MAAa,iBAAiB;CAAC;CAAQ;CAAQ;CAAS;CAAmB;CAAoB;CAAa;AAAY;AA2BxH,MAAa,SAA6C;CACzD,MAAM;EACL,OAAO;EACP,KAAK;EACL,QAAQ;EACR,MAAM;EACN,SAAS;EACT,SAAS;EACT,OAAO;EACP,MAAM;EACN,UAAU;EACV,SAAS;CACV;CACA,OAAO;EACN,OAAO;EACP,KAAK;EACL,QAAQ;EACR,MAAM;EACN,SAAS;EACT,SAAS;EACT,OAAO;EACP,MAAM;EACN,UAAU;EACV,SAAS;CACV;CAEA,mBAAmB;EAClB,OAAO;EACP,KAAK;EACL,QAAQ;EACR,MAAM;EACN,SAAS;EACT,SAAS;EACT,OAAO;EACP,MAAM;EACN,UAAU;EACV,SAAS;CACV;CACA,oBAAoB;EACnB,OAAO;EACP,KAAK;EACL,QAAQ;EACR,MAAM;EACN,SAAS;EACT,SAAS;EACT,OAAO;EACP,MAAM;EACN,UAAU;EACV,SAAS;CACV;CAEA,aAAa;EACZ,OAAO;EACP,KAAK;EACL,QAAQ;EACR,MAAM;EACN,SAAS;EACT,SAAS;EACT,OAAO;EACP,MAAM;EACN,UAAU;EACV,SAAS;CACV;CACA,cAAc;EACb,OAAO;EACP,KAAK;EACL,QAAQ;EACR,MAAM;EACN,SAAS;EACT,SAAS;EACT,OAAO;EACP,MAAM;EACN,UAAU;EACV,SAAS;CACV;AACD;AAEA,SAAgB,eAAe,OAAuC;CACrE,OAAO,OAAO,UAAU,YAAa,eAAqC,SAAS,KAAK;AACzF;;;;;AAMA,SAAgB,yBAAyB,MAAyB,QAAQ,KAAuB;CAChG,MAAM,aAAa,IAAI,WAAW,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE;CAClD,IAAI,eAAe,UAAa,CAAC,QAAQ,KAAK,UAAU,GAAG,OAAO;CAClE,MAAM,QAAQ,OAAO,UAAU;CAC/B,OAAO,UAAU,KAAK,UAAU,KAAK,UAAU;AAChD;AAEA,SAAgB,aAAa,SAAuB,MAAyB,QAAQ,KAAgB;CACpG,OAAO,YAAY,SAAS,yBAAyB,GAAG,IAAI;AAC7D;;;;;AAcA,SAAgB,oBAAoB,EAAE,MAAM,MAAM,QAAQ,KAAK,SAA4C;CAC1G,IAAI,eAAe,IAAI,GAAG,OAAO;CACjC,MAAM,UAAU,IAAI,aAAa,KAAK,CAAC,CAAC,YAAY;CACpD,IAAI,eAAe,OAAO,GAAG,OAAO;CACpC,OAAO,SAAS;AACjB;;AAGA,SAAgB,gBAAgB,MAAyB,QAAQ,KAAa;CAC7E,MAAM,YACL,IAAI,oBACJ,MAAM,IAAI,oBAAoB,QAAQ,aAAa,UAAU,IAAI,UAAU,YAAe,KAAK,QAAQ,GAAG,SAAS,GAAG,OAAO;CAC9H,OAAO,KAAK,WAAW,kBAAkB;AAC1C;AAEA,SAAgB,eAAe,MAAyB,QAAQ,KAA+B;CAC9F,IAAI;EACH,MAAM,EAAE,UAAU,KAAK,MAAM,aAAa,gBAAgB,GAAG,GAAG,MAAM,CAAC;EACvE,OAAO,eAAe,KAAK,IAAI,QAAQ;CACxC,QAAQ;EACP;CACD;AACD;;AAGA,SAAgB,UAAU,SAAuB,MAAyB,QAAQ,KAAc;CAC/F,MAAM,OAAO,gBAAgB,GAAG;CAChC,IAAI;EACH,IAAI,WAAoC,CAAC;EACzC,IAAI;GACH,MAAM,SAAkB,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;GAC7D,IAAI,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,GAAG,WAAW;EAChF,QAAQ,CAER;EACA,UAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;EAC5C,cAAc,MAAM,GAAG,KAAK,UAAU;GAAE,GAAG;GAAU,OAAO;EAAQ,GAAG,MAAM,GAAI,EAAE,GAAG;EACtF,OAAO;CACR,QAAQ;EACP,OAAO;CACR;AACD"}
@@ -0,0 +1,3 @@
1
+ import { n as resolveTscBinary, t as TscBuilder } from "./tsc-D56BJ6lA.js";
2
+
3
+ export { TscBuilder };
@@ -96,6 +96,13 @@ var ProcessSupervisor = class extends EventEmitter {
96
96
  });
97
97
  return this.#stopping;
98
98
  }
99
+ /**
100
+ * Sends `SIGKILL` right away, without waiting for the process to exit. Used to force a shutdown that is taking too long.
101
+ */
102
+ kill() {
103
+ const child = this.#child;
104
+ if (child && child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
105
+ }
99
106
  async restart() {
100
107
  await this.stop();
101
108
  this.start();
@@ -252,4 +259,4 @@ function resolveTscBinary(root) {
252
259
 
253
260
  //#endregion
254
261
  export { createLineSplitter as i, resolveTscBinary as n, ProcessSupervisor as r, TscBuilder as t };
255
- //# sourceMappingURL=tsc-BTuFXw3O.js.map
262
+ //# sourceMappingURL=tsc-D56BJ6lA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tsc-D56BJ6lA.js","names":[],"sources":["../src/lib/process-supervisor.ts","../src/lib/builders/tsc.ts"],"sourcesContent":["import { spawn, type ChildProcess } from 'node:child_process';\nimport { EventEmitter } from 'node:events';\n\nexport type ProcessState = 'idle' | 'starting' | 'running' | 'stopping' | 'stopped' | 'crashed';\n\nexport interface ProcessExit {\n\tcode: number | null;\n\tsignal: NodeJS.Signals | null;\n\t/** Whether the exit was requested through {@link ProcessSupervisor.stop}. */\n\trequested: boolean;\n}\n\nexport interface ProcessSupervisorOptions {\n\tcommand: string;\n\targs: readonly string[];\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n\t/** Milliseconds to wait after `SIGTERM` before sending `SIGKILL`. */\n\tkillTimeout: number;\n}\n\nexport interface ProcessSupervisorEvents {\n\tstate: [state: ProcessState];\n\tstdout: [line: string];\n\tstderr: [line: string];\n\texit: [exit: ProcessExit];\n\terror: [error: Error];\n}\n\n/**\n * Owns a single child process: starts it, streams its output line by line and stops it gracefully.\n */\nexport class ProcessSupervisor extends EventEmitter<ProcessSupervisorEvents> {\n\t#child: ChildProcess | null = null;\n\t#state: ProcessState = 'idle';\n\t#startedAt: number | null = null;\n\t#stopping: Promise<void> | null = null;\n\n\tpublic constructor(private readonly options: ProcessSupervisorOptions) {\n\t\tsuper();\n\t}\n\n\tpublic get state(): ProcessState {\n\t\treturn this.#state;\n\t}\n\n\tpublic get pid(): number | null {\n\t\treturn this.#child?.pid ?? null;\n\t}\n\n\tpublic get startedAt(): number | null {\n\t\treturn this.#startedAt;\n\t}\n\n\tpublic get running(): boolean {\n\t\treturn this.#state === 'starting' || this.#state === 'running';\n\t}\n\n\tpublic start(): void {\n\t\tif (this.#child) throw new Error('The process is already running, call stop() or restart() first.');\n\n\t\tconst { command, args, cwd, env } = this.options;\n\t\tthis.#setState('starting');\n\n\t\tconst child = spawn(command, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });\n\t\tthis.#child = child;\n\t\tthis.#startedAt = Date.now();\n\n\t\tlet requested = false;\n\t\tconst stdout = createLineSplitter((line) => this.emit('stdout', line));\n\t\tconst stderr = createLineSplitter((line) => this.emit('stderr', line));\n\t\tchild.stdout?.on('data', stdout.push);\n\t\tchild.stderr?.on('data', stderr.push);\n\n\t\tchild.once('spawn', () => {\n\t\t\tif (this.#child === child) this.#setState('running');\n\t\t});\n\n\t\tchild.once('error', (error) => {\n\t\t\tthis.emit('error', error);\n\t\t});\n\n\t\tchild.once('exit', (code, signal) => {\n\t\t\tstdout.flush();\n\t\t\tstderr.flush();\n\t\t\trequested = this.#state === 'stopping';\n\t\t\tthis.#child = null;\n\t\t\tthis.#startedAt = null;\n\t\t\tthis.#setState(requested || code === 0 ? 'stopped' : 'crashed');\n\t\t\tthis.emit('exit', { code, signal, requested });\n\t\t});\n\t}\n\n\t/**\n\t * Sends `SIGTERM`, escalates to `SIGKILL` after `killTimeout` and resolves once the process exited.\n\t */\n\tpublic stop(): Promise<void> {\n\t\tconst child = this.#child;\n\t\tif (!child) return Promise.resolve();\n\t\tif (this.#stopping) return this.#stopping;\n\n\t\tthis.#setState('stopping');\n\t\tthis.#stopping = new Promise<void>((resolve) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tif (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');\n\t\t\t}, this.options.killTimeout);\n\t\t\ttimer.unref();\n\n\t\t\tchild.once('exit', () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tthis.#stopping = null;\n\t\t\t\tresolve();\n\t\t\t});\n\n\t\t\tif (!child.kill('SIGTERM')) {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tthis.#stopping = null;\n\t\t\t\tresolve();\n\t\t\t}\n\t\t});\n\n\t\treturn this.#stopping;\n\t}\n\n\t/**\n\t * Sends `SIGKILL` right away, without waiting for the process to exit. Used to force a shutdown that is taking too long.\n\t */\n\tpublic kill(): void {\n\t\tconst child = this.#child;\n\t\tif (child && child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');\n\t}\n\n\tpublic async restart(): Promise<void> {\n\t\tawait this.stop();\n\t\tthis.start();\n\t}\n\n\t#setState(state: ProcessState): void {\n\t\tif (this.#state === state) return;\n\t\tthis.#state = state;\n\t\tthis.emit('state', state);\n\t}\n}\n\n/**\n * Splits a byte stream into complete lines, keeping the trailing partial line until the next chunk.\n */\nexport function createLineSplitter(onLine: (line: string) => void) {\n\tlet remainder = '';\n\treturn {\n\t\tpush(chunk: Buffer | string) {\n\t\t\tremainder += chunk.toString();\n\t\t\tconst lines = remainder.split(/\\r?\\n/);\n\t\t\tremainder = lines.pop() ?? '';\n\t\t\tfor (const line of lines) onLine(line);\n\t\t},\n\t\tflush() {\n\t\t\tif (remainder.length > 0) onLine(remainder);\n\t\t\tremainder = '';\n\t\t}\n\t};\n}\n","import { spawn, type ChildProcess } from 'node:child_process';\nimport { EventEmitter } from 'node:events';\nimport type { ResolvedStarsConfig } from '@wolfstar/http-framework/config';\nimport { CliError } from '../errors.js';\nimport { createLineSplitter } from '../process-supervisor.js';\nimport { resolveBinary } from '../project.js';\nimport type { Builder, BuilderEvents, BuildOutcome } from './types.js';\n\nconst INSTALL_HINT = \"Install it with `pnpm add -D typescript`, or set `build.tool` to 'tsdown' or 'none'.\";\nconst WATCH_START = /Starting (incremental )?compilation/;\nconst WATCH_DONE = /Found (\\d+) errors?\\. Watching for file changes/;\nconst ERROR_LINE = /error TS\\d+:/;\n\n/**\n * Builds through the project's `tsc -b`, parsing its output to report progress.\n */\nexport class TscBuilder extends EventEmitter<BuilderEvents> implements Builder {\n\tpublic readonly tool = 'tsc' as const;\n\t#child: ChildProcess | null = null;\n\t#startedAt = 0;\n\t#firstError: string | null = null;\n\n\tpublic constructor(private readonly config: ResolvedStarsConfig) {\n\t\tsuper();\n\t}\n\n\tpublic build(): Promise<BuildOutcome> {\n\t\tconst child = this.#spawn([]);\n\t\tthis.#begin();\n\n\t\treturn new Promise((resolve) => {\n\t\t\tchild.once('exit', (code) => {\n\t\t\t\tthis.#child = null;\n\t\t\t\tresolve(this.#finish(code === 0 ? null : (this.#firstError ?? `tsc exited with code ${code}`)));\n\t\t\t});\n\t\t});\n\t}\n\n\tpublic watch(): Promise<void> {\n\t\tthis.#spawn(['--watch', '--preserveWatchOutput']);\n\t\tthis.#begin();\n\t\treturn Promise.resolve();\n\t}\n\n\tpublic close(): Promise<void> {\n\t\tconst child = this.#child;\n\t\tthis.#child = null;\n\t\tif (!child || child.exitCode !== null) return Promise.resolve();\n\n\t\treturn new Promise((resolve) => {\n\t\t\tchild.once('exit', () => resolve());\n\t\t\tchild.kill();\n\t\t});\n\t}\n\n\t#spawn(extraArgs: string[]): ChildProcess {\n\t\tconst tsc = resolveTscBinary(this.config.root);\n\t\tif (!tsc) throw new CliError(`\"typescript\" is not installed in ${this.config.root}`, { code: 'DEPENDENCY_MISSING', hint: INSTALL_HINT });\n\n\t\tconst child = spawn(process.execPath, [tsc, '-b', this.config.build.tsconfig!, '--pretty', 'false', ...extraArgs], {\n\t\t\tcwd: this.config.root,\n\t\t\tenv: process.env,\n\t\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t\t\twindowsHide: true\n\t\t});\n\t\tthis.#child = child;\n\n\t\tconst handleLine = (line: string) => this.#handleLine(line);\n\t\tconst stdout = createLineSplitter(handleLine);\n\t\tconst stderr = createLineSplitter(handleLine);\n\t\tchild.stdout?.on('data', stdout.push);\n\t\tchild.stderr?.on('data', stderr.push);\n\t\tchild.once('exit', () => {\n\t\t\tstdout.flush();\n\t\t\tstderr.flush();\n\t\t});\n\t\tchild.once('error', (error) => this.emit('log', 'error', error.message));\n\n\t\treturn child;\n\t}\n\n\t#handleLine(line: string): void {\n\t\tconst text = line.trim();\n\t\tif (text.length === 0) return;\n\n\t\tif (WATCH_START.test(text)) {\n\t\t\tif (this.#startedAt === 0) this.#begin();\n\t\t\treturn;\n\t\t}\n\n\t\tif (ERROR_LINE.test(text)) {\n\t\t\tthis.#firstError ??= text;\n\t\t\tthis.emit('log', 'error', text);\n\t\t\treturn;\n\t\t}\n\n\t\tconst done = WATCH_DONE.exec(text);\n\t\tif (done) {\n\t\t\tconst errors = Number(done[1]);\n\t\t\tthis.emit('log', errors === 0 ? 'success' : 'error', text);\n\t\t\tthis.#finish(errors === 0 ? null : (this.#firstError ?? text));\n\t\t\treturn;\n\t\t}\n\n\t\tthis.emit('log', 'info', text);\n\t}\n\n\t#begin(): void {\n\t\tthis.#firstError = null;\n\t\tthis.#startedAt = performance.now();\n\t\tthis.emit('start');\n\t}\n\n\t#finish(message: string | null): BuildOutcome {\n\t\tconst outcome: BuildOutcome = { ok: message === null, durationMs: Math.round(performance.now() - this.#startedAt), message };\n\t\tthis.#startedAt = 0;\n\t\tthis.emit(outcome.ok ? 'success' : 'failure', outcome);\n\t\treturn outcome;\n\t}\n}\n\n/**\n * Locates the `tsc` script of the project's TypeScript through its `package.json#bin`, which works for both the\n * classic `lib/tsc.js` layout (TypeScript ≤ 6) and the native `bin/tsc` launcher (TypeScript 7, whose `exports` map\n * hides `lib/`).\n */\nexport function resolveTscBinary(root: string): string | null {\n\treturn resolveBinary(root, 'typescript', 'tsc');\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { t as readOwnPackageJson } from "./version-DN6Uho-Y.js";
2
+ import { n as THEME_SETTINGS, o as resolveTheme, r as detectTerminalBackground, t as THEMES } from "./theme-Bosd2Kk2.js";
2
3
  import { n as classifyAppLine, r as isErrorDetail } from "./log-buffer-D-nj2ZEU.js";
3
4
  import { execFile } from "node:child_process";
4
5
  import { stripVTControlCharacters } from "node:util";
@@ -9,22 +10,31 @@ import { StringDecoder } from "node:string_decoder";
9
10
 
10
11
  //#region src/renderers/ink/theme.tsx
11
12
  /**
12
- * Whether the UI may use colour. `stars dev` decides this once (`NO_COLOR`, `FORCE_COLOR`, TTY detection) and every
13
- * component reads it from here instead of re-deriving it.
13
+ * `stars dev` decides once whether the UI may use colour (`NO_COLOR`, `FORCE_COLOR`, TTY detection) and which theme
14
+ * to paint with; every component reads both from here instead of re-deriving them.
14
15
  */
15
- const ColorContext = createContext(true);
16
- function ColorProvider({ color, children }) {
17
- return /* @__PURE__ */ jsx(ColorContext, {
18
- value: color,
16
+ const ThemeContext = createContext({
17
+ color: true,
18
+ theme: "dark"
19
+ });
20
+ function ThemeProvider({ color, theme, children }) {
21
+ return /* @__PURE__ */ jsx(ThemeContext, {
22
+ value: {
23
+ color,
24
+ theme
25
+ },
19
26
  children
20
27
  });
21
28
  }
22
29
  /**
23
- * Returns a colour name only when colour is enabled, so `<Text color={useColor()('green')}>` degrades to plain text.
30
+ * Returns a theme colour only when colour is enabled, so `<Text color={usePaint()('success')}>` degrades to plain text.
24
31
  */
25
- function useColor() {
26
- const enabled = useContext(ColorContext);
27
- return (name) => enabled ? name : void 0;
32
+ function usePaint() {
33
+ const { color, theme } = useContext(ThemeContext);
34
+ return (token) => color ? THEMES[theme][token] : void 0;
35
+ }
36
+ function useColorEnabled() {
37
+ return useContext(ThemeContext).color;
28
38
  }
29
39
 
30
40
  //#endregion
@@ -34,6 +44,7 @@ const KEYS = [
34
44
  ["o", "open the local URL in a browser"],
35
45
  ["t", "toggle the public tunnel"],
36
46
  ["i", "show versions, URLs and session info"],
47
+ ["T", "pick a colour theme (dark, light, colour-blind friendly, ANSI)"],
37
48
  ["l", "open the logs (scroll, filter by source or level)"],
38
49
  ["e", "jump to the last error, keeping its context"],
39
50
  ["c / Ctrl+L", "clear the log history"],
@@ -42,7 +53,7 @@ const KEYS = [
42
53
  ];
43
54
  /** The static keys reference, opened with `?` from the pinned panel. */
44
55
  function HelpOverlay({ height, width, onClose }) {
45
- const paint = useColor();
56
+ const paint = usePaint();
46
57
  useInput((input, key) => {
47
58
  if (!key.ctrl && (key.escape || [
48
59
  "q",
@@ -68,7 +79,7 @@ function HelpOverlay({ height, width, onClose }) {
68
79
  " ",
69
80
  /* @__PURE__ */ jsx(Text, {
70
81
  bold: true,
71
- color: paint("white"),
82
+ color: paint("text"),
72
83
  children: key.padEnd(14)
73
84
  }),
74
85
  /* @__PURE__ */ jsx(Text, {
@@ -127,6 +138,11 @@ const PANEL_HINTS = [
127
138
  label: "info",
128
139
  priority: 50
129
140
  },
141
+ {
142
+ key: "T",
143
+ label: "theme",
144
+ priority: 30
145
+ },
130
146
  {
131
147
  key: "l",
132
148
  label: "logs",
@@ -175,7 +191,7 @@ function Hints({ hints, width }) {
175
191
  const group = (entry) => entry.source === "app" ? "runtime" : entry.source === "build" || entry.source === "tsc" ? "build" : "cli";
176
192
  /** Filterable history, using Nuxt's keys and selecting the last error without discarding its context. */
177
193
  function LogBrowser({ service, height, width, lastError = false, onClose, onCopy }) {
178
- const paint = useColor();
194
+ const paint = usePaint();
179
195
  const [sources, setSources] = useState({
180
196
  cli: true,
181
197
  build: true,
@@ -294,10 +310,10 @@ function LogBrowser({ service, height, width, lastError = false, onClose, onCopy
294
310
  }), visible.map(({ entry, text }, i) => /* @__PURE__ */ jsxs(Text, {
295
311
  wrap: "truncate-end",
296
312
  children: [/* @__PURE__ */ jsx(Text, {
297
- color: paint("green"),
313
+ color: paint("brand"),
298
314
  children: entry.id === selected ? "▎ " : " "
299
315
  }), /* @__PURE__ */ jsx(Text, {
300
- color: paint(entry.level === "error" ? "red" : entry.level === "warn" ? "yellow" : entry.level === "success" ? "green" : "white"),
316
+ color: paint(entry.level === "error" ? "error" : entry.level === "warn" ? "warning" : entry.level === "success" ? "success" : "text"),
301
317
  dimColor: isErrorDetail(entry),
302
318
  children: text
303
319
  })]
@@ -474,7 +490,7 @@ function InfoOverlay({ service, height, width, onClose }) {
474
490
  //#region src/renderers/ink/components/Panel.tsx
475
491
  /** Nuxt's wordmark / URLs / progress / status / hints layout, with Stars branding. */
476
492
  function Panel({ status, config, counters, frame, elapsedMs, width, height, confirmQuit }) {
477
- const paint = useColor();
493
+ const paint = usePaint();
478
494
  const state = describeBadge(status);
479
495
  const busy = state.badge !== "ready" && state.badge !== "error";
480
496
  const badge = !busy && counters.errors > 0 ? "error" : state.badge;
@@ -489,7 +505,7 @@ function Panel({ status, config, counters, frame, elapsedMs, width, height, conf
489
505
  wrap: "truncate-end",
490
506
  children: [" ", line === null ? /* @__PURE__ */ jsxs(Fragment, { children: [
491
507
  /* @__PURE__ */ jsx(Text, {
492
- color: paint("green"),
508
+ color: paint("brand"),
493
509
  children: [..."·✦★✦"].map((cell, i) => /* @__PURE__ */ jsx(Text, {
494
510
  dimColor: busy && i !== frame % 4,
495
511
  children: cell
@@ -498,7 +514,7 @@ function Panel({ status, config, counters, frame, elapsedMs, width, height, conf
498
514
  " ",
499
515
  /* @__PURE__ */ jsx(Text, {
500
516
  bold: true,
501
- color: paint("green"),
517
+ color: paint("brand"),
502
518
  children: "Stars"
503
519
  }),
504
520
  /* @__PURE__ */ jsx(Text, {
@@ -506,7 +522,7 @@ function Panel({ status, config, counters, frame, elapsedMs, width, height, conf
506
522
  children: ` ${readOwnPackageJson().version}`
507
523
  })
508
524
  ] }) : /* @__PURE__ */ jsx(Text, {
509
- color: paint("green"),
525
+ color: paint("brand"),
510
526
  children: line
511
527
  })]
512
528
  }), index === 0 && width >= 60 && badge === "ready" && status.progress.readyMs !== null && /* @__PURE__ */ jsx(Text, {
@@ -530,12 +546,12 @@ function Panel({ status, config, counters, frame, elapsedMs, width, height, conf
530
546
  children: "Local "
531
547
  }),
532
548
  /* @__PURE__ */ jsx(Text, {
533
- color: paint("cyan"),
549
+ color: paint("url"),
534
550
  dimColor: busy,
535
551
  children: status.url
536
552
  }),
537
553
  busy && /* @__PURE__ */ jsx(Text, {
538
- color: paint("yellow"),
554
+ color: paint("busy"),
539
555
  children: ` ${[
540
556
  "⠋",
541
557
  "⠙",
@@ -563,7 +579,7 @@ function Panel({ status, config, counters, frame, elapsedMs, width, height, conf
563
579
  children: "Tunnel "
564
580
  }),
565
581
  /* @__PURE__ */ jsx(Text, {
566
- color: paint("magenta"),
582
+ color: paint("tunnel"),
567
583
  children: status.tunnelUrl
568
584
  })
569
585
  ]
@@ -580,7 +596,7 @@ function Panel({ status, config, counters, frame, elapsedMs, width, height, conf
580
596
  children: [
581
597
  " ",
582
598
  /* @__PURE__ */ jsx(Text, {
583
- color: paint("green"),
599
+ color: paint("brand"),
584
600
  children: "━".repeat(filled)
585
601
  }),
586
602
  /* @__PURE__ */ jsxs(Text, {
@@ -593,12 +609,12 @@ function Panel({ status, config, counters, frame, elapsedMs, width, height, conf
593
609
  children: [
594
610
  " ",
595
611
  counters.warnings > 0 && /* @__PURE__ */ jsx(Text, {
596
- color: paint("yellow"),
612
+ color: paint("warning"),
597
613
  children: `⚠ ${counters.warnings} ${plural(counters.warnings, "warning")} `
598
614
  }),
599
615
  counters.errors > 0 && /* @__PURE__ */ jsx(Text, {
600
616
  bold: true,
601
- color: paint("red"),
617
+ color: paint("error"),
602
618
  children: `✖ ${counters.errors} ${plural(counters.errors, "error")} `
603
619
  }),
604
620
  counters.errors === 0 && counters.warnings === 0 && /* @__PURE__ */ jsx(Text, {
@@ -620,8 +636,8 @@ function Panel({ status, config, counters, frame, elapsedMs, width, height, conf
620
636
  " ",
621
637
  /* @__PURE__ */ jsx(Text, {
622
638
  bold: true,
623
- backgroundColor: paint(confirmQuit || busy ? "yellow" : badge === "error" ? "red" : "green"),
624
- color: paint(badge === "error" && !confirmQuit ? "white" : "black"),
639
+ backgroundColor: paint(confirmQuit || busy ? "busy" : badge === "error" ? "error" : "success"),
640
+ color: paint(badge === "error" && !confirmQuit ? "onError" : "onBright"),
625
641
  children: ` ${confirmQuit ? "QUIT?" : badge === "restarting" ? "RESTART" : badge.toUpperCase()} `
626
642
  }),
627
643
  " ",
@@ -754,16 +770,101 @@ function openDevUrl(value) {
754
770
  return new Promise((resolve, reject) => execFile(command, [...args], (error) => error ? reject(/* @__PURE__ */ new Error(`Could not open the browser: ${error.message}`)) : resolve()));
755
771
  }
756
772
 
773
+ //#endregion
774
+ //#region src/renderers/ink/components/ThemeOverlay.tsx
775
+ const DESCRIPTIONS = {
776
+ auto: "match the terminal background",
777
+ dark: "for dark terminals",
778
+ light: "for light terminals",
779
+ "dark-daltonized": "dark, colour-blind friendly (blue/orange)",
780
+ "light-daltonized": "light, colour-blind friendly (blue/orange)",
781
+ "dark-ansi": "dark, only your terminal palette",
782
+ "light-ansi": "light, only your terminal palette"
783
+ };
784
+ /** Like Claude Code's `/theme`: arrows preview live, `Enter` keeps the choice and saves it, `Esc` restores. */
785
+ function ThemeOverlay({ current, height, width, onPreview, onSelect, onClose }) {
786
+ const color = useColorEnabled();
787
+ const [index, setIndex] = useState(Math.max(0, THEME_SETTINGS.indexOf(current)));
788
+ const move = (delta) => {
789
+ const next = (index + delta + THEME_SETTINGS.length) % THEME_SETTINGS.length;
790
+ setIndex(next);
791
+ onPreview(THEME_SETTINGS[next]);
792
+ };
793
+ useInput((input, key) => {
794
+ if (key.ctrl) return;
795
+ if (key.escape || input === "q" || input === "T") return onClose();
796
+ if (key.return) return onSelect(THEME_SETTINGS[index]);
797
+ if (key.upArrow || input === "k") return move(-1);
798
+ if (key.downArrow || input === "j") return move(1);
799
+ });
800
+ return /* @__PURE__ */ jsxs(Box, {
801
+ flexDirection: "column",
802
+ height,
803
+ children: [
804
+ /* @__PURE__ */ jsx(Text, {
805
+ bold: true,
806
+ children: " colour theme"
807
+ }),
808
+ /* @__PURE__ */ jsx(Text, {
809
+ dimColor: true,
810
+ children: "─".repeat(width)
811
+ }),
812
+ THEME_SETTINGS.slice(0, Math.max(0, height - 3)).map((setting, i) => {
813
+ const swatch = THEMES[setting === "auto" ? detectTerminalBackground() : setting];
814
+ return /* @__PURE__ */ jsxs(Text, {
815
+ wrap: "truncate-end",
816
+ children: [
817
+ i === index ? " ▎ " : " ",
818
+ /* @__PURE__ */ jsx(Text, {
819
+ bold: i === index,
820
+ children: setting.padEnd(18)
821
+ }),
822
+ color && /* @__PURE__ */ jsxs(Text, { children: [
823
+ /* @__PURE__ */ jsx(Text, {
824
+ color: swatch.success,
825
+ children: "● "
826
+ }),
827
+ /* @__PURE__ */ jsx(Text, {
828
+ color: swatch.warning,
829
+ children: "● "
830
+ }),
831
+ /* @__PURE__ */ jsx(Text, {
832
+ color: swatch.error,
833
+ children: "● "
834
+ }),
835
+ /* @__PURE__ */ jsx(Text, {
836
+ color: swatch.url,
837
+ children: "● "
838
+ })
839
+ ] }),
840
+ /* @__PURE__ */ jsx(Text, {
841
+ dimColor: true,
842
+ children: ` ${DESCRIPTIONS[setting]}${setting === current ? " (current)" : ""}`
843
+ })
844
+ ]
845
+ }, setting);
846
+ }),
847
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
848
+ /* @__PURE__ */ jsx(Text, {
849
+ dimColor: true,
850
+ children: " ↑/↓ preview · enter save · esc cancel"
851
+ })
852
+ ]
853
+ });
854
+ }
855
+
757
856
  //#endregion
758
857
  //#region src/renderers/ink/DevApp.tsx
759
858
  /**
760
859
  * A bottom-aligned panel in the normal buffer, with logs folded away. Only the browsable overlays use the
761
860
  * alternate screen, preserving the terminal's history and the panel when they close.
762
861
  */
763
- function DevApp({ service, color, reducedMotion, onQuit, onViewChange, onCopy }) {
862
+ function DevApp({ service, color, theme, reducedMotion, onQuit, onViewChange, onCopy, onThemeSave }) {
764
863
  const { columns, rows } = useWindowSize();
765
864
  const [view, setView] = useState("panel");
766
865
  const [confirmQuit, setConfirmQuit] = useState(false);
866
+ const [saved, setSaved] = useState(theme);
867
+ const [preview, setPreview] = useState(theme);
767
868
  const status = useDevStatus(service);
768
869
  const counters = useLogCounters(service);
769
870
  const badge = describeBadge(status).badge;
@@ -800,12 +901,14 @@ function DevApp({ service, color, reducedMotion, onQuit, onViewChange, onCopy })
800
901
  service.toggleTunnel();
801
902
  return;
802
903
  case "i": return changeView("info");
904
+ case "T": return changeView("theme");
803
905
  case "h":
804
906
  case "?": return changeView("help");
805
907
  }
806
908
  });
807
- return /* @__PURE__ */ jsx(ColorProvider, {
909
+ return /* @__PURE__ */ jsx(ThemeProvider, {
808
910
  color,
911
+ theme: resolveTheme(preview),
809
912
  children: /* @__PURE__ */ jsxs(Box, {
810
913
  width,
811
914
  height,
@@ -831,6 +934,22 @@ function DevApp({ service, color, reducedMotion, onQuit, onViewChange, onCopy })
831
934
  width,
832
935
  onClose: () => changeView("panel")
833
936
  }),
937
+ view === "theme" && /* @__PURE__ */ jsx(ThemeOverlay, {
938
+ current: saved,
939
+ height,
940
+ width,
941
+ onPreview: setPreview,
942
+ onSelect: (setting) => {
943
+ setSaved(setting);
944
+ setPreview(setting);
945
+ onThemeSave(setting);
946
+ changeView("panel");
947
+ },
948
+ onClose: () => {
949
+ setPreview(saved);
950
+ changeView("panel");
951
+ }
952
+ }),
834
953
  view === "panel" && /* @__PURE__ */ jsx(Panel, {
835
954
  status,
836
955
  config: service.config,
@@ -909,6 +1028,8 @@ function createTuiRenderer(service, options = {}) {
909
1028
  const instance = render(/* @__PURE__ */ jsx(DevApp, {
910
1029
  service,
911
1030
  color: options.color ?? false,
1031
+ theme: options.theme ?? "auto",
1032
+ onThemeSave: options.onThemeSave ?? (() => {}),
912
1033
  reducedMotion: options.reducedMotion ?? false,
913
1034
  onQuit: quit,
914
1035
  onViewChange: switchView,
@@ -944,4 +1065,4 @@ function createTuiRenderer(service, options = {}) {
944
1065
 
945
1066
  //#endregion
946
1067
  export { createTuiRenderer };
947
- //# sourceMappingURL=tui-BCGQrMQo.js.map
1068
+ //# sourceMappingURL=tui-O94OH1w8.js.map