@texuli/tools 0.0.0-stage → 0.1.0-next.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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -2
  3. package/dist/a2ui/from-a2ui.d.ts +35 -0
  4. package/dist/a2ui/from-a2ui.js +180 -0
  5. package/dist/a2ui/index.d.ts +11 -0
  6. package/dist/a2ui/index.js +11 -0
  7. package/dist/a2ui/props.d.ts +10 -0
  8. package/dist/a2ui/props.js +94 -0
  9. package/dist/a2ui/schema.d.ts +26 -0
  10. package/dist/a2ui/schema.js +193 -0
  11. package/dist/a2ui/to-a2ui.d.ts +30 -0
  12. package/dist/a2ui/to-a2ui.js +119 -0
  13. package/dist/a2ui/types.d.ts +71 -0
  14. package/dist/a2ui/types.js +11 -0
  15. package/dist/cli-add/add.d.ts +21 -0
  16. package/dist/cli-add/add.js +241 -0
  17. package/dist/cli-add/cli.d.ts +25 -0
  18. package/dist/cli-add/cli.js +157 -0
  19. package/dist/cli-add/index.d.ts +12 -0
  20. package/dist/cli-add/index.js +11 -0
  21. package/dist/cli-add/paths.d.ts +4 -0
  22. package/dist/cli-add/paths.js +4 -0
  23. package/dist/cli-add/provenance.d.ts +32 -0
  24. package/dist/cli-add/provenance.js +78 -0
  25. package/dist/cli-add/registry.d.ts +19 -0
  26. package/dist/cli-add/registry.js +50 -0
  27. package/dist/cli-add/types.d.ts +71 -0
  28. package/dist/cli-add/types.js +4 -0
  29. package/dist/cli-init/cli.d.ts +25 -0
  30. package/dist/cli-init/cli.js +212 -0
  31. package/dist/cli-init/config.d.ts +10 -0
  32. package/dist/cli-init/config.js +62 -0
  33. package/dist/cli-init/detect.d.ts +13 -0
  34. package/dist/cli-init/detect.js +42 -0
  35. package/dist/cli-init/files.d.ts +18 -0
  36. package/dist/cli-init/files.js +59 -0
  37. package/dist/cli-init/index.d.ts +14 -0
  38. package/dist/cli-init/index.js +13 -0
  39. package/dist/cli-init/init.d.ts +14 -0
  40. package/dist/cli-init/init.js +157 -0
  41. package/dist/cli-init/safe-path.d.ts +2 -0
  42. package/dist/cli-init/safe-path.js +26 -0
  43. package/dist/cli-init/theme-apply.d.ts +2 -0
  44. package/dist/cli-init/theme-apply.js +69 -0
  45. package/dist/cli-init/themes.d.ts +37 -0
  46. package/dist/cli-init/themes.js +83 -0
  47. package/dist/cli-init/types.d.ts +50 -0
  48. package/dist/cli-init/types.js +1 -0
  49. package/dist/cli-init/wire.d.ts +20 -0
  50. package/dist/cli-init/wire.js +90 -0
  51. package/dist/cli-update/cli.d.ts +26 -0
  52. package/dist/cli-update/cli.js +161 -0
  53. package/dist/cli-update/index.d.ts +11 -0
  54. package/dist/cli-update/index.js +10 -0
  55. package/dist/cli-update/lines.d.ts +38 -0
  56. package/dist/cli-update/lines.js +189 -0
  57. package/dist/cli-update/provenance.d.ts +14 -0
  58. package/dist/cli-update/provenance.js +51 -0
  59. package/dist/cli-update/types.d.ts +83 -0
  60. package/dist/cli-update/types.js +1 -0
  61. package/dist/cli-update/update.d.ts +12 -0
  62. package/dist/cli-update/update.js +261 -0
  63. package/dist/elements/content.d.ts +15 -0
  64. package/dist/elements/content.js +53 -0
  65. package/dist/elements/define.d.ts +49 -0
  66. package/dist/elements/define.js +281 -0
  67. package/dist/elements/index.d.ts +6 -0
  68. package/dist/elements/index.js +6 -0
  69. package/dist/golden/axe.d.ts +32 -0
  70. package/dist/golden/axe.js +22 -0
  71. package/dist/golden/catalog.d.ts +24 -0
  72. package/dist/golden/catalog.js +85 -0
  73. package/dist/golden/cli.d.ts +9 -0
  74. package/dist/golden/cli.js +45 -0
  75. package/dist/golden/guidance.d.ts +12 -0
  76. package/dist/golden/guidance.js +171 -0
  77. package/dist/golden/index.d.ts +17 -0
  78. package/dist/golden/index.js +17 -0
  79. package/dist/golden/outline.d.ts +21 -0
  80. package/dist/golden/outline.js +68 -0
  81. package/dist/golden/paint.d.ts +21 -0
  82. package/dist/golden/paint.js +103 -0
  83. package/dist/golden/report.d.ts +28 -0
  84. package/dist/golden/report.js +64 -0
  85. package/dist/golden/score.d.ts +16 -0
  86. package/dist/golden/score.js +116 -0
  87. package/dist/golden/sinks.d.ts +8 -0
  88. package/dist/golden/sinks.js +11 -0
  89. package/dist/golden/tasks.d.ts +4 -0
  90. package/dist/golden/tasks.js +686 -0
  91. package/dist/golden/types.d.ts +54 -0
  92. package/dist/golden/types.js +9 -0
  93. package/dist/index.d.ts +8 -0
  94. package/dist/index.js +8 -0
  95. package/dist/internal/paths.d.ts +56 -0
  96. package/dist/internal/paths.js +186 -0
  97. package/dist/llms/bundle.d.ts +6 -0
  98. package/dist/llms/bundle.js +47 -0
  99. package/dist/llms/cli.d.ts +24 -0
  100. package/dist/llms/cli.js +104 -0
  101. package/dist/llms/format.d.ts +35 -0
  102. package/dist/llms/format.js +166 -0
  103. package/dist/llms/index.d.ts +9 -0
  104. package/dist/llms/index.js +8 -0
  105. package/dist/llms/types.d.ts +51 -0
  106. package/dist/llms/types.js +1 -0
  107. package/dist/llms/write.d.ts +14 -0
  108. package/dist/llms/write.js +49 -0
  109. package/dist/mcp/index.d.ts +2 -0
  110. package/dist/mcp/index.js +2 -0
  111. package/dist/mcp/server.d.ts +20 -0
  112. package/dist/mcp/server.js +326 -0
  113. package/dist/mcp/stdio.d.ts +3 -0
  114. package/dist/mcp/stdio.js +34 -0
  115. package/package.json +63 -4
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Provenance header stamped on every file `texuli add` writes.
3
+ * Later `texuli update` / `diff` tasks read this block.
4
+ */
5
+ export type ProvenanceMeta = {
6
+ id: string;
7
+ package: string;
8
+ version: string;
9
+ /** Source path relative to the monorepo/package root. */
10
+ source: string;
11
+ };
12
+ /** Build the multi-line provenance comment body (no surrounding markers). */
13
+ export declare function provenanceBody(meta: ProvenanceMeta): string;
14
+ /** Block comment for TypeScript / CSS / JavaScript. */
15
+ export declare function provenanceBlockComment(meta: ProvenanceMeta): string;
16
+ /** HTML comment for Svelte markup files (safe above `<script>`). */
17
+ export declare function provenanceHtmlComment(meta: ProvenanceMeta): string;
18
+ /** Choose a header form from the destination file extension. */
19
+ export declare function provenanceHeader(meta: ProvenanceMeta, fileName: string): string;
20
+ /**
21
+ * The `source` a header records: the component directory relative to the
22
+ * source root (both real paths, computed by the caller) joined with the file's
23
+ * relative path, using `/`. Returns null when the result is not a plain
24
+ * relative path inside the root: empty, absolute, a `.` or `..` segment, a
25
+ * backslash, or a character that would break the header (`\0`, a line break,
26
+ * a comment end).
27
+ */
28
+ export declare function provenanceSource(sourceDir: string, relativePath: string): string | null;
29
+ /** Whether `source` is safe to stamp: relative, inside the root, header-safe. */
30
+ export declare function isPlainSourcePath(source: string): boolean;
31
+ /** True when file contents already carry a texuli add provenance block. */
32
+ export declare function hasProvenance(content: string): boolean;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Provenance header stamped on every file `texuli add` writes.
3
+ * Later `texuli update` / `diff` tasks read this block.
4
+ */
5
+ const BEGIN = '@texuli/add — copied styled source. Do not remove this block.';
6
+ /** Build the multi-line provenance comment body (no surrounding markers). */
7
+ export function provenanceBody(meta) {
8
+ return [
9
+ BEGIN,
10
+ `id: ${meta.id}`,
11
+ `package: ${meta.package}`,
12
+ `version: ${meta.version}`,
13
+ `source: ${meta.source}`,
14
+ ].join('\n');
15
+ }
16
+ /** Block comment for TypeScript / CSS / JavaScript. */
17
+ export function provenanceBlockComment(meta) {
18
+ const lines = provenanceBody(meta)
19
+ .split('\n')
20
+ .map((line) => ` * ${line}`)
21
+ .join('\n');
22
+ return `/**\n${lines}\n */\n`;
23
+ }
24
+ /** HTML comment for Svelte markup files (safe above `<script>`). */
25
+ export function provenanceHtmlComment(meta) {
26
+ return `<!--\n${provenanceBody(meta)
27
+ .split('\n')
28
+ .map((line) => ` ${line}`)
29
+ .join('\n')}\n-->\n`;
30
+ }
31
+ /** Choose a header form from the destination file extension. */
32
+ export function provenanceHeader(meta, fileName) {
33
+ const lower = fileName.toLowerCase();
34
+ if (lower.endsWith('.svelte'))
35
+ return provenanceHtmlComment(meta);
36
+ if (lower.endsWith('.ts') ||
37
+ lower.endsWith('.js') ||
38
+ lower.endsWith('.css') ||
39
+ lower.endsWith('.mjs') ||
40
+ lower.endsWith('.cjs')) {
41
+ return provenanceBlockComment(meta);
42
+ }
43
+ // Other text files get a # style header when they look like scripts; else HTML.
44
+ if (lower.endsWith('.md') || lower.endsWith('.txt')) {
45
+ return provenanceBody(meta)
46
+ .split('\n')
47
+ .map((line) => `# ${line}`)
48
+ .join('\n')
49
+ .concat('\n');
50
+ }
51
+ return provenanceHtmlComment(meta);
52
+ }
53
+ /**
54
+ * The `source` a header records: the component directory relative to the
55
+ * source root (both real paths, computed by the caller) joined with the file's
56
+ * relative path, using `/`. Returns null when the result is not a plain
57
+ * relative path inside the root: empty, absolute, a `.` or `..` segment, a
58
+ * backslash, or a character that would break the header (`\0`, a line break,
59
+ * a comment end).
60
+ */
61
+ export function provenanceSource(sourceDir, relativePath) {
62
+ const source = [sourceDir, relativePath].filter((part) => part.length > 0).join('/');
63
+ return isPlainSourcePath(source) ? source : null;
64
+ }
65
+ /** Whether `source` is safe to stamp: relative, inside the root, header-safe. */
66
+ export function isPlainSourcePath(source) {
67
+ if (source.length === 0 || source.startsWith('/') || /^[A-Za-z]:/.test(source))
68
+ return false;
69
+ if (/[\\\0\r\n]/.test(source) || source.includes('*/') || source.includes('-->'))
70
+ return false;
71
+ return source
72
+ .split('/')
73
+ .every((segment) => segment !== '' && segment !== '.' && segment !== '..');
74
+ }
75
+ /** True when file contents already carry a texuli add provenance block. */
76
+ export function hasProvenance(content) {
77
+ return content.includes('@texuli/add');
78
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Styled components `texuli add` may copy. Paths are under the owning package
3
+ * root and never under `packages/primitives`.
4
+ */
5
+ import type { AddableComponent } from './types.js';
6
+ /**
7
+ * Built-in addable set: styled `texuli` components only.
8
+ * Primitives, tokens and engines are intentionally absent.
9
+ */
10
+ export declare const DEFAULT_ADD_REGISTRY: readonly AddableComponent[];
11
+ /** Look up a styled component by id, or `undefined` when unknown / not addable. */
12
+ export declare function getAddable(id: string, registry?: readonly AddableComponent[]): AddableComponent | undefined;
13
+ /** Every addable id (sorted). */
14
+ export declare function listAddableIds(registry?: readonly AddableComponent[]): string[];
15
+ /**
16
+ * True when a path segment would pull primitives (forbidden for add).
17
+ * Used as a belt-and-braces check on resolved source paths.
18
+ */
19
+ export declare function isPrimitivesPath(absolutePath: string): boolean;
@@ -0,0 +1,50 @@
1
+ const TEXULI = 'texuli';
2
+ function texuliComponent(id, folder = id, peers) {
3
+ return {
4
+ id,
5
+ package: TEXULI,
6
+ directory: `src/components/${folder}`,
7
+ ...(peers ? { peers } : {}),
8
+ };
9
+ }
10
+ /**
11
+ * Built-in addable set: styled `texuli` components only.
12
+ * Primitives, tokens and engines are intentionally absent.
13
+ */
14
+ export const DEFAULT_ADD_REGISTRY = Object.freeze([
15
+ texuliComponent('alert'),
16
+ texuliComponent('badge'),
17
+ texuliComponent('button', 'button', ['texuli/cx']),
18
+ texuliComponent('card', 'card', ['texuli/cx']),
19
+ texuliComponent('empty'),
20
+ texuliComponent('field'),
21
+ texuliComponent('input'),
22
+ texuliComponent('kbd-code', 'kbd-code'),
23
+ texuliComponent('stat'),
24
+ texuliComponent('stepper', 'stepper', ['texuli/cx', 'texuli/button']),
25
+ texuliComponent('table', 'table', ['texuli/cx']),
26
+ texuliComponent('tabs-accordion', 'tabs-accordion', ['texuli/cx']),
27
+ texuliComponent('toast', 'toast', ['texuli/cx']),
28
+ texuliComponent('dialog', 'dialog', ['texuli/cx', 'texuli/button', 'texuli/icons']),
29
+ ]);
30
+ const BY_ID = new Map(DEFAULT_ADD_REGISTRY.map((entry) => [entry.id, entry]));
31
+ /** Look up a styled component by id, or `undefined` when unknown / not addable. */
32
+ export function getAddable(id, registry = DEFAULT_ADD_REGISTRY) {
33
+ if (registry === DEFAULT_ADD_REGISTRY)
34
+ return BY_ID.get(id);
35
+ return registry.find((entry) => entry.id === id);
36
+ }
37
+ /** Every addable id (sorted). */
38
+ export function listAddableIds(registry = DEFAULT_ADD_REGISTRY) {
39
+ return registry.map((entry) => entry.id).sort();
40
+ }
41
+ /**
42
+ * True when a path segment would pull primitives (forbidden for add).
43
+ * Used as a belt-and-braces check on resolved source paths.
44
+ */
45
+ export function isPrimitivesPath(absolutePath) {
46
+ const normalized = absolutePath.replace(/\\/g, '/');
47
+ return (/\/packages\/primitives(\/|$)/.test(normalized) ||
48
+ /\/@texuli\/primitives(\/|$)/.test(normalized) ||
49
+ /\/primitives\/src\//.test(normalized));
50
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Types for `texuli add` — copy styled component source into a consumer app.
3
+ */
4
+ /** One file the add command will write. */
5
+ export type AddFilePlan = {
6
+ /** Path relative to the component source directory. */
7
+ relativePath: string;
8
+ /** Absolute path of the source file. */
9
+ from: string;
10
+ /** Absolute path of the destination file. */
11
+ to: string;
12
+ /** Bytes of the source before the provenance header. */
13
+ sourceBytes: number;
14
+ };
15
+ /** Result of one `add` run (human or `--json`). */
16
+ export type AddResult = {
17
+ ok: boolean;
18
+ /** Component id requested. */
19
+ id: string;
20
+ /** Package that owns the styled source. */
21
+ package: string;
22
+ /** Directory written under the consumer project. */
23
+ outDir: string;
24
+ /** Files written or that would be written (`dryRun`). */
25
+ files: Array<{
26
+ path: string;
27
+ action: 'write' | 'skip' | 'would-write';
28
+ bytes: number;
29
+ }>;
30
+ errors: string[];
31
+ /** True when nothing was written because of `--dry-run`. */
32
+ dryRun: boolean;
33
+ };
34
+ /** A styled component the add command may copy. Never primitives. */
35
+ export type AddableComponent = {
36
+ /** Closed kebab-case id (matches catalog when present). */
37
+ id: string;
38
+ /** Workspace package name, e.g. `texuli`. */
39
+ package: string;
40
+ /**
41
+ * Directory under the package root that holds the styled source
42
+ * (for example `src/components/button`). Copied recursively.
43
+ */
44
+ directory: string;
45
+ /** Optional peer import hints for the consumer. */
46
+ peers?: readonly string[];
47
+ };
48
+ export type AddOptions = {
49
+ /** Component id to copy. */
50
+ id: string;
51
+ /** Consumer project root (files land under `outDir`). */
52
+ cwd: string;
53
+ /**
54
+ * Absolute path to the monorepo (or fixture) root that holds
55
+ * `packages/<pkg>/…` source. No network — local paths only.
56
+ */
57
+ sourceRoot: string;
58
+ /**
59
+ * Destination directory relative to `cwd`
60
+ * (default `src/components/texuli/<id>`).
61
+ */
62
+ outDir?: string;
63
+ /** Package version stamped into the provenance header. */
64
+ version?: string;
65
+ /** When true, refuse to overwrite existing files. Default true. */
66
+ force?: boolean;
67
+ /** Plan only; write nothing. */
68
+ dryRun?: boolean;
69
+ /** Override the built-in registry (tests). */
70
+ registry?: readonly AddableComponent[];
71
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Types for `texuli add` — copy styled component source into a consumer app.
3
+ */
4
+ export {};
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import type { Framework } from './types.js';
3
+ export declare const USAGE = "Usage:\n texuli init [options] Wire Texuli tokens and CSS into a Vite, Astro or SvelteKit project\n texuli theme list [--search <q>] List the theme library\n texuli theme preview <id> Show a theme's key tokens and its WCAG AA report\n texuli theme apply <id> Switch an initialized project to another theme\n\nOptions:\n --cwd <dir> Project root (default: current directory)\n --framework <name> vite | astro | sveltekit (default: detected from package.json)\n --theme <id> Theme for init (default: enterprise-neutral)\n --css <path> Stylesheet path, relative to the project (default per framework)\n --namespace <id> No-flash appearance namespace (default: app)\n --mode <light|dark> Preview mode (default: light)\n --search <words> Filter theme list\n --force Replace a stylesheet or config texuli did not generate (never follows symlinks)\n --dry-run Plan only; write nothing\n --json Machine-readable output\n --help Show this help\n";
4
+ export interface InitCliArgs {
5
+ command: 'init' | 'theme-list' | 'theme-preview' | 'theme-apply' | 'help';
6
+ id: string | null;
7
+ cwd: string;
8
+ framework: Framework | null;
9
+ theme: string | null;
10
+ css: string | null;
11
+ namespace: string | null;
12
+ mode: 'light' | 'dark';
13
+ search: string | null;
14
+ force: boolean;
15
+ dryRun: boolean;
16
+ json: boolean;
17
+ }
18
+ /** Parses `init …` or `theme <list|preview|apply> …`. Throws on bad input. */
19
+ export declare function parseInitArgs(argv: readonly string[], cwd?: string): InitCliArgs;
20
+ /** Runs the CLI; returns the exit code. `io` defaults to the console. */
21
+ export declare function runInitCli(argv: readonly string[], io?: {
22
+ cwd?: string;
23
+ out?: (line: string) => void;
24
+ err?: (line: string) => void;
25
+ }): Promise<number>;
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ /**
4
+ * `texuli init` and `texuli theme`. No network.
5
+ *
6
+ * pnpm exec tsx packages/tools/src/cli-init/cli.ts init [--cwd <app>] [--framework vite|astro|sveltekit]
7
+ * [--theme <id>] [--css <path>] [--namespace <id>] [--force] [--dry-run] [--json]
8
+ * pnpm exec tsx packages/tools/src/cli-init/cli.ts theme list [--search <words>] [--json]
9
+ * pnpm exec tsx packages/tools/src/cli-init/cli.ts theme preview <id> [--mode light|dark] [--json]
10
+ * pnpm exec tsx packages/tools/src/cli-init/cli.ts theme apply <id> [--cwd <app>] [--force] [--dry-run] [--json]
11
+ */
12
+ import { fileURLToPath } from 'node:url';
13
+ import { FRAMEWORKS } from "./detect.js";
14
+ import { initProject } from "./init.js";
15
+ import { applyProjectTheme } from "./theme-apply.js";
16
+ import { listThemes, previewTheme } from "./themes.js";
17
+ export const USAGE = `Usage:
18
+ texuli init [options] Wire Texuli tokens and CSS into a Vite, Astro or SvelteKit project
19
+ texuli theme list [--search <q>] List the theme library
20
+ texuli theme preview <id> Show a theme's key tokens and its WCAG AA report
21
+ texuli theme apply <id> Switch an initialized project to another theme
22
+
23
+ Options:
24
+ --cwd <dir> Project root (default: current directory)
25
+ --framework <name> vite | astro | sveltekit (default: detected from package.json)
26
+ --theme <id> Theme for init (default: enterprise-neutral)
27
+ --css <path> Stylesheet path, relative to the project (default per framework)
28
+ --namespace <id> No-flash appearance namespace (default: app)
29
+ --mode <light|dark> Preview mode (default: light)
30
+ --search <words> Filter theme list
31
+ --force Replace a stylesheet or config texuli did not generate (never follows symlinks)
32
+ --dry-run Plan only; write nothing
33
+ --json Machine-readable output
34
+ --help Show this help
35
+ `;
36
+ /** Parses `init …` or `theme <list|preview|apply> …`. Throws on bad input. */
37
+ export function parseInitArgs(argv, cwd = process.cwd()) {
38
+ const rest = [...argv];
39
+ const args = {
40
+ command: 'help',
41
+ id: null,
42
+ cwd,
43
+ framework: null,
44
+ theme: null,
45
+ css: null,
46
+ namespace: null,
47
+ mode: 'light',
48
+ search: null,
49
+ force: false,
50
+ dryRun: false,
51
+ json: false,
52
+ };
53
+ const verb = rest.shift();
54
+ if (verb === 'init')
55
+ args.command = 'init';
56
+ else if (verb === 'theme') {
57
+ const sub = rest.shift();
58
+ if (sub === 'list')
59
+ args.command = 'theme-list';
60
+ else if (sub === 'preview')
61
+ args.command = 'theme-preview';
62
+ else if (sub === 'apply')
63
+ args.command = 'theme-apply';
64
+ else
65
+ throw new Error(`Unknown theme command: ${sub ?? '(none)'}`);
66
+ }
67
+ else if (verb !== undefined && verb !== '--help' && verb !== '-h') {
68
+ throw new Error(`Unknown command: ${verb}`);
69
+ }
70
+ for (let index = 0; index < rest.length; index += 1) {
71
+ const arg = rest[index];
72
+ const value = () => {
73
+ const next = rest[index + 1];
74
+ if (next === undefined || next.startsWith('--'))
75
+ throw new Error(`${arg} needs a value`);
76
+ index += 1;
77
+ return next;
78
+ };
79
+ if (arg === '--help' || arg === '-h')
80
+ args.command = 'help';
81
+ else if (arg === '--json')
82
+ args.json = true;
83
+ else if (arg === '--force')
84
+ args.force = true;
85
+ else if (arg === '--dry-run')
86
+ args.dryRun = true;
87
+ else if (arg === '--cwd')
88
+ args.cwd = value();
89
+ else if (arg === '--theme')
90
+ args.theme = value();
91
+ else if (arg === '--css')
92
+ args.css = value();
93
+ else if (arg === '--namespace')
94
+ args.namespace = value();
95
+ else if (arg === '--search')
96
+ args.search = value();
97
+ else if (arg === '--framework') {
98
+ const name = value();
99
+ if (!FRAMEWORKS.includes(name))
100
+ throw new Error(`--framework must be one of ${FRAMEWORKS.join(', ')}`);
101
+ args.framework = name;
102
+ }
103
+ else if (arg === '--mode') {
104
+ const mode = value();
105
+ if (mode !== 'light' && mode !== 'dark')
106
+ throw new Error('--mode must be light or dark');
107
+ args.mode = mode;
108
+ }
109
+ else if (arg.startsWith('-'))
110
+ throw new Error(`Unknown option: ${arg}`);
111
+ else if (args.id === null &&
112
+ (args.command === 'theme-preview' || args.command === 'theme-apply'))
113
+ args.id = arg;
114
+ else
115
+ throw new Error(`Unexpected argument: ${arg}`);
116
+ }
117
+ if ((args.command === 'theme-preview' || args.command === 'theme-apply') && !args.id) {
118
+ throw new Error('A theme id is required.');
119
+ }
120
+ return args;
121
+ }
122
+ function printResult(result, out, err) {
123
+ for (const error of result.errors)
124
+ err(error);
125
+ for (const file of result.files) {
126
+ out(` ${(result.dryRun && file.action !== 'unchanged' ? `would ${file.action}` : file.action).padEnd(14)} ${file.path}`);
127
+ }
128
+ for (const note of result.notes)
129
+ out(`note: ${note}`);
130
+ }
131
+ /** Runs the CLI; returns the exit code. `io` defaults to the console. */
132
+ export async function runInitCli(argv, io = {}) {
133
+ const out = io.out ?? ((line) => console.log(line));
134
+ const err = io.err ?? ((line) => console.error(line));
135
+ let args;
136
+ try {
137
+ args = parseInitArgs(argv, io.cwd);
138
+ }
139
+ catch (error) {
140
+ err(error instanceof Error ? error.message : String(error));
141
+ err(USAGE);
142
+ return 1;
143
+ }
144
+ switch (args.command) {
145
+ case 'help':
146
+ out(USAGE);
147
+ return 0;
148
+ case 'theme-list': {
149
+ const themes = await listThemes(args.search ?? undefined);
150
+ if (args.json)
151
+ out(JSON.stringify({ themes }));
152
+ else
153
+ for (const theme of themes)
154
+ out(`${theme.id.padEnd(28)} ${theme.name} (${theme.collection}, ${theme.contrastLevel}${theme.darkFirst ? ', dark-first' : ''})`);
155
+ return 0;
156
+ }
157
+ case 'theme-preview': {
158
+ const preview = await previewTheme(args.id, args.mode);
159
+ if (!preview) {
160
+ err(`Unknown theme: ${args.id}. Run \`texuli theme list\`.`);
161
+ return 1;
162
+ }
163
+ if (args.json)
164
+ out(JSON.stringify(preview));
165
+ else {
166
+ out(`${preview.name} (${preview.id}), ${preview.mode}: ${preview.description}`);
167
+ for (const [name, value] of Object.entries(preview.tokens))
168
+ out(` ${name.padEnd(26)} ${value}`);
169
+ out(`WCAG AA: ${preview.aa.pass ? 'pass' : `${preview.aa.issues} issue(s)`} (${preview.aa.modes.map((mode) => `${mode.mode} ${mode.pass ? 'pass' : `${mode.issues} issue(s)`}`).join(', ')})`);
170
+ }
171
+ return 0;
172
+ }
173
+ case 'theme-apply':
174
+ case 'init': {
175
+ const result = args.command === 'init'
176
+ ? await initProject({
177
+ cwd: args.cwd,
178
+ ...(args.framework ? { framework: args.framework } : {}),
179
+ ...(args.theme ? { theme: args.theme } : {}),
180
+ ...(args.css ? { css: args.css } : {}),
181
+ ...(args.namespace ? { namespace: args.namespace } : {}),
182
+ force: args.force,
183
+ dryRun: args.dryRun,
184
+ })
185
+ : await applyProjectTheme({
186
+ cwd: args.cwd,
187
+ theme: args.id,
188
+ force: args.force,
189
+ dryRun: args.dryRun,
190
+ });
191
+ if (args.json)
192
+ out(JSON.stringify(result));
193
+ else
194
+ printResult(result, out, err);
195
+ return result.ok ? 0 : 1;
196
+ }
197
+ }
198
+ }
199
+ function isMain(metaUrl) {
200
+ const entry = process.argv[1];
201
+ try {
202
+ return entry !== undefined && fileURLToPath(metaUrl) === entry;
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ }
208
+ if (isMain(import.meta.url)) {
209
+ runInitCli(process.argv.slice(2)).then((code) => {
210
+ process.exitCode = code;
211
+ });
212
+ }
@@ -0,0 +1,10 @@
1
+ import type { TexuliConfig } from './types.js';
2
+ export declare const CONFIG_FILE = "texuli.config.json";
3
+ /** Parses and validates `texuli.config.json`; returns errors for anything unsafe or unknown. */
4
+ export declare function parseConfig(text: string): {
5
+ config: TexuliConfig;
6
+ } | {
7
+ errors: string[];
8
+ };
9
+ export declare function formatConfig(config: TexuliConfig): string;
10
+ export declare function isId(value: string): boolean;
@@ -0,0 +1,62 @@
1
+ import { FRAMEWORKS } from "./detect.js";
2
+ import { projectPathError } from "./safe-path.js";
3
+ export const CONFIG_FILE = 'texuli.config.json';
4
+ const ID = /^[a-z][a-z0-9-]{0,79}$/;
5
+ /** Parses and validates `texuli.config.json`; returns errors for anything unsafe or unknown. */
6
+ export function parseConfig(text) {
7
+ let raw;
8
+ try {
9
+ raw = JSON.parse(text);
10
+ }
11
+ catch (error) {
12
+ return {
13
+ errors: [
14
+ `${CONFIG_FILE} is not JSON: ${error instanceof Error ? error.message : String(error)}`,
15
+ ],
16
+ };
17
+ }
18
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
19
+ return { errors: [`${CONFIG_FILE} must be a JSON object.`] };
20
+ }
21
+ const record = raw;
22
+ const errors = [];
23
+ if (!FRAMEWORKS.includes(record.framework)) {
24
+ errors.push(`${CONFIG_FILE} framework must be one of ${FRAMEWORKS.join(', ')}.`);
25
+ }
26
+ for (const field of ['css', 'entry', 'document']) {
27
+ const error = projectPathError(`${CONFIG_FILE} ${field}`, record[field]);
28
+ if (error)
29
+ errors.push(error);
30
+ }
31
+ for (const field of ['theme', 'namespace']) {
32
+ if (typeof record[field] !== 'string' || !ID.test(record[field])) {
33
+ errors.push(`${CONFIG_FILE} ${field} must be a kebab-case id.`);
34
+ }
35
+ }
36
+ if (errors.length > 0)
37
+ return { errors };
38
+ return {
39
+ config: {
40
+ framework: record.framework,
41
+ css: record.css,
42
+ entry: record.entry,
43
+ document: record.document,
44
+ theme: record.theme,
45
+ namespace: record.namespace,
46
+ },
47
+ };
48
+ }
49
+ export function formatConfig(config) {
50
+ const ordered = {
51
+ framework: config.framework,
52
+ css: config.css,
53
+ entry: config.entry,
54
+ document: config.document,
55
+ theme: config.theme,
56
+ namespace: config.namespace,
57
+ };
58
+ return `${JSON.stringify(ordered, null, 2)}\n`;
59
+ }
60
+ export function isId(value) {
61
+ return ID.test(value);
62
+ }
@@ -0,0 +1,13 @@
1
+ import type { Framework } from './types.js';
2
+ export declare const FRAMEWORKS: readonly Framework[];
3
+ /**
4
+ * The framework a `package.json` declares, most specific first: SvelteKit
5
+ * (it also uses Vite), then Astro, then Vite. `null` when none is declared.
6
+ */
7
+ export declare function detectFramework(packageJson: unknown): Framework | null;
8
+ /** Default file locations per framework (relative, forward slashes). */
9
+ export declare const DEFAULTS: Record<Framework, {
10
+ css: string;
11
+ entries: readonly string[];
12
+ document: readonly string[];
13
+ }>;
@@ -0,0 +1,42 @@
1
+ export const FRAMEWORKS = ['sveltekit', 'astro', 'vite'];
2
+ /**
3
+ * The framework a `package.json` declares, most specific first: SvelteKit
4
+ * (it also uses Vite), then Astro, then Vite. `null` when none is declared.
5
+ */
6
+ export function detectFramework(packageJson) {
7
+ if (typeof packageJson !== 'object' || packageJson === null)
8
+ return null;
9
+ const record = packageJson;
10
+ const names = new Set();
11
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
12
+ const deps = record[field];
13
+ if (typeof deps === 'object' && deps !== null)
14
+ for (const name of Object.keys(deps))
15
+ names.add(name);
16
+ }
17
+ if (names.has('@sveltejs/kit'))
18
+ return 'sveltekit';
19
+ if (names.has('astro'))
20
+ return 'astro';
21
+ if (names.has('vite'))
22
+ return 'vite';
23
+ return null;
24
+ }
25
+ /** Default file locations per framework (relative, forward slashes). */
26
+ export const DEFAULTS = {
27
+ vite: {
28
+ css: 'src/texuli.css',
29
+ entries: ['src/main.ts', 'src/main.js'],
30
+ document: ['index.html'],
31
+ },
32
+ sveltekit: {
33
+ css: 'src/texuli.css',
34
+ entries: ['src/routes/+layout.svelte'],
35
+ document: ['src/app.html'],
36
+ },
37
+ astro: {
38
+ css: 'src/styles/texuli.css',
39
+ entries: ['src/layouts/Layout.astro', 'src/layouts/BaseLayout.astro'],
40
+ document: ['src/layouts/Layout.astro', 'src/layouts/BaseLayout.astro'],
41
+ },
42
+ };
@@ -0,0 +1,18 @@
1
+ import type { FileChange } from './types.js';
2
+ export interface Project {
3
+ /** Real path of the project root. */
4
+ root: string;
5
+ }
6
+ export declare function openProject(cwd: string): Project | string;
7
+ /** Resolves a project-relative path, or explains why it is refused. */
8
+ export declare function resolveInProject(project: Project, rel: string, name?: string): string | {
9
+ error: string;
10
+ };
11
+ /** Reads a project file (null when missing), refusing paths outside the project or through symlinks. */
12
+ export declare function readProjectFile(project: Project, rel: string, name?: string): Promise<string | null | {
13
+ error: string;
14
+ }>;
15
+ /** A planned write: compare, then (unless dry run) write through the containment checks again. */
16
+ export declare function writeProjectFile(project: Project, rel: string, before: string | null, after: string, dryRun: boolean): Promise<FileChange | {
17
+ error: string;
18
+ }>;