@proto.ui/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Proto UI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # @proto.ui/cli
2
+
3
+ Local CLI package for generating Proto UI Tailwind assets.
4
+
5
+ ## Commands
6
+
7
+ - `proto-ui tokens --input <dir> --out <file>`
8
+ - `proto-ui tailwindcss --out <file> [--theme-import <path>] [--tokens-import <path>]`
9
+ - `proto-ui theme shadcn --out <file>`
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/cli.mjs';
3
+
4
+ run(process.argv.slice(2)).catch((error) => {
5
+ const message = error instanceof Error ? error.message : String(error);
6
+ console.error(`[proto-ui] ${message}`);
7
+ process.exit(1);
8
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { parseArgs } from 'node:util';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ async function readPackageVersion() {
9
+ const pkgPath = join(__dirname, '..', 'package.json');
10
+ const raw = await readFile(pkgPath, 'utf8');
11
+ const pkg = JSON.parse(raw);
12
+ return pkg.version ?? '0.0.0';
13
+ }
14
+ function printHelp(version) {
15
+ console.log(`@proto-ui/cli v${version}
16
+
17
+ 用法:
18
+ npx @proto-ui/cli <command> [options]
19
+
20
+ 命令:
21
+ init 在当前目录创建 proto-ui/ 目录结构
22
+ add <adapter> <prototype>
23
+ 登记要添加的组件(完整生成流程后续版本提供)
24
+ help, --help, -h 显示本说明
25
+ version, --version, -v
26
+ 打印版本号
27
+
28
+ 示例:
29
+ npx @proto-ui/cli init
30
+ npx @proto-ui/cli add react shadcn-button
31
+ `);
32
+ }
33
+ async function cmdInit(cwd) {
34
+ const root = join(cwd, 'proto-ui');
35
+ if (existsSync(root)) {
36
+ console.error('proto-ui/ 已存在,跳过创建。如需重建请先手动删除该目录。');
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+ const subdirs = ['adapters', 'prototypes', 'components'];
41
+ for (const d of subdirs) {
42
+ await mkdir(join(root, d), { recursive: true });
43
+ }
44
+ // Make proto-ui/ a local file dependency so users can import `proto-ui/components`
45
+ const localPkgJson = {
46
+ name: 'proto-ui',
47
+ version: '0.0.0',
48
+ private: true,
49
+ type: 'module',
50
+ exports: {
51
+ './components': {
52
+ types: './components/index.d.ts',
53
+ default: './components/index.js',
54
+ },
55
+ },
56
+ };
57
+ await writeFile(join(root, 'package.json'), JSON.stringify(localPkgJson, null, 2) + '\n', 'utf8');
58
+ const componentsIndexJs = `export * from './generated.js';\n`;
59
+ const componentsGeneratedJs = `// generated by @proto-ui/cli\nexport {};\n`;
60
+ await writeFile(join(root, 'components', 'index.js'), componentsIndexJs, 'utf8');
61
+ await writeFile(join(root, 'components', 'generated.js'), componentsGeneratedJs, 'utf8');
62
+ const componentsIndexDts = `export * from './generated';\n`;
63
+ const componentsGeneratedDts = `// generated by @proto-ui/cli\nexport {};\n`;
64
+ await writeFile(join(root, 'components', 'index.d.ts'), componentsIndexDts, 'utf8');
65
+ await writeFile(join(root, 'components', 'generated.d.ts'), componentsGeneratedDts, 'utf8');
66
+ const readme = `# Proto UI(由 @proto-ui/cli init 生成)
67
+
68
+ - adapters/ — 当前项目使用的适配器
69
+ - prototypes/ — 安装的原型
70
+ - components/ — 组装后的组件入口
71
+
72
+ 这些内容通常由 CLI 维护;直接编辑前请查阅文档。
73
+ `;
74
+ await writeFile(join(root, 'README.md'), readme, 'utf8');
75
+ console.log(`已创建 ${subdirs.map((d) => `proto-ui/${d}/`).join('、')}`);
76
+ console.log('下一步: 将本地包 proto-ui 挂到你的项目依赖里(推荐 file:)。');
77
+ console.log('例如(pnpm): pnpm add -D \"proto-ui@file:./proto-ui\"');
78
+ console.log('然后你就可以: import { ... } from \"proto-ui/components\"');
79
+ }
80
+ const vuePrototypeMap = {
81
+ // shadcn
82
+ 'shadcn-button': {
83
+ lib: '@prototype-libs/shadcn',
84
+ libExportName: 'shadcnButton',
85
+ componentExportName: 'Button',
86
+ },
87
+ 'shadcn-toggle': {
88
+ lib: '@prototype-libs/shadcn',
89
+ libExportName: 'shadcnToggle',
90
+ componentExportName: 'Toggle',
91
+ },
92
+ 'shadcn-switch-root': {
93
+ lib: '@prototype-libs/shadcn',
94
+ libExportName: 'shadcnSwitchRoot',
95
+ componentExportName: 'SwitchRoot',
96
+ },
97
+ 'shadcn-switch-thumb': {
98
+ lib: '@prototype-libs/shadcn',
99
+ libExportName: 'shadcnSwitchThumb',
100
+ componentExportName: 'SwitchThumb',
101
+ },
102
+ 'shadcn-tabs-root': {
103
+ lib: '@prototype-libs/shadcn',
104
+ libExportName: 'shadcnTabsRoot',
105
+ componentExportName: 'TabsRoot',
106
+ },
107
+ 'shadcn-tabs-list': {
108
+ lib: '@prototype-libs/shadcn',
109
+ libExportName: 'shadcnTabsList',
110
+ componentExportName: 'TabsList',
111
+ },
112
+ 'shadcn-tabs-trigger': {
113
+ lib: '@prototype-libs/shadcn',
114
+ libExportName: 'shadcnTabsTrigger',
115
+ componentExportName: 'TabsTrigger',
116
+ },
117
+ 'shadcn-tabs-content': {
118
+ lib: '@prototype-libs/shadcn',
119
+ libExportName: 'shadcnTabsContent',
120
+ componentExportName: 'TabsContent',
121
+ },
122
+ 'shadcn-hover-card-root': {
123
+ lib: '@prototype-libs/shadcn',
124
+ libExportName: 'shadcnHoverCardRoot',
125
+ componentExportName: 'HoverCardRoot',
126
+ },
127
+ 'shadcn-hover-card-trigger': {
128
+ lib: '@prototype-libs/shadcn',
129
+ libExportName: 'shadcnHoverCardTrigger',
130
+ componentExportName: 'HoverCardTrigger',
131
+ },
132
+ 'shadcn-hover-card-content': {
133
+ lib: '@prototype-libs/shadcn',
134
+ libExportName: 'shadcnHoverCardContent',
135
+ componentExportName: 'HoverCardContent',
136
+ },
137
+ // base
138
+ 'base-button': {
139
+ lib: '@prototype-libs/base',
140
+ libExportName: 'button',
141
+ componentExportName: 'BaseButton',
142
+ },
143
+ };
144
+ function buildVueGeneratedJs(resolved) {
145
+ if (resolved.length === 0)
146
+ return `// generated by @proto-ui/cli\nexport {};\n`;
147
+ // manifest 可能会因为重复 add 产生重复请求;在生成阶段按导出名去重,避免重复导出导致的错误
148
+ const byExportName = new Map();
149
+ for (const r of resolved) {
150
+ if (!byExportName.has(r.componentExportName))
151
+ byExportName.set(r.componentExportName, r);
152
+ }
153
+ const dedupedResolved = Array.from(byExportName.values());
154
+ const exportNames = dedupedResolved.map((r) => r.componentExportName);
155
+ const byLib = new Map();
156
+ for (const r of dedupedResolved) {
157
+ if (!byLib.has(r.lib))
158
+ byLib.set(r.lib, new Set());
159
+ byLib.get(r.lib).add(r.libExportName);
160
+ }
161
+ const libImports = Array.from(byLib.entries())
162
+ .map(([lib, exportsSet]) => {
163
+ const list = Array.from(exportsSet).sort((a, b) => a.localeCompare(b));
164
+ return `import { ${list.join(', ')} } from '${lib}';`;
165
+ })
166
+ .join('\n');
167
+ const resolvedSorted = dedupedResolved.sort((a, b) => a.componentExportName.localeCompare(b.componentExportName));
168
+ const lines = [];
169
+ lines.push(`import { createVueAdapter } from '@proto-ui/adapters.vue';`);
170
+ lines.push(`import * as Vue from 'vue';`);
171
+ lines.push(libImports);
172
+ lines.push('');
173
+ lines.push(`const AdaptToVue = createVueAdapter(Vue);`);
174
+ lines.push('');
175
+ for (const r of resolvedSorted) {
176
+ lines.push(`const ${r.componentExportName} = AdaptToVue(${r.libExportName});`);
177
+ }
178
+ lines.push('');
179
+ lines.push(`export { ${resolvedSorted.map((r) => r.componentExportName).join(', ')} };`);
180
+ lines.push('');
181
+ return lines.join('\n');
182
+ }
183
+ function buildVueGeneratedDts(resolved) {
184
+ if (resolved.length === 0)
185
+ return `// generated by @proto-ui/cli\nexport {};\n`;
186
+ const names = Array.from(new Set(resolved.map((r) => r.componentExportName))).sort((a, b) => a.localeCompare(b));
187
+ return (`// generated by @proto-ui/cli\n` + names.map((n) => `export const ${n}: any;`).join('\n') + '\n');
188
+ }
189
+ async function cmdAdd(cwd, adapter, prototype) {
190
+ const root = join(cwd, 'proto-ui');
191
+ if (!existsSync(root)) {
192
+ console.error('未找到 proto-ui/。请先运行: npx @proto-ui/cli init');
193
+ process.exitCode = 1;
194
+ return;
195
+ }
196
+ const manifestPath = join(root, 'manifest.json');
197
+ let manifest = { requests: [] };
198
+ if (existsSync(manifestPath)) {
199
+ try {
200
+ const parsed = JSON.parse(await readFile(manifestPath, 'utf8'));
201
+ if (parsed?.requests && Array.isArray(parsed.requests)) {
202
+ manifest = parsed;
203
+ }
204
+ }
205
+ catch {
206
+ // ignore corrupt manifest
207
+ }
208
+ }
209
+ manifest.requests.push({
210
+ adapter,
211
+ prototype,
212
+ addedAt: new Date().toISOString(),
213
+ });
214
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
215
+ console.log(`已记录: ${adapter} / ${prototype}(写入 proto-ui/manifest.json)`);
216
+ if (adapter === 'vue') {
217
+ const resolved = [];
218
+ for (const req of manifest.requests) {
219
+ if (req.adapter !== 'vue')
220
+ continue;
221
+ const map = vuePrototypeMap[req.prototype];
222
+ if (!map) {
223
+ const available = Object.keys(vuePrototypeMap).join(', ');
224
+ throw new Error(`[@proto-ui/cli] 未知原型: ${req.prototype}\n可用 Vue 原型: ${available}`);
225
+ }
226
+ resolved.push(map);
227
+ }
228
+ const generatedJs = buildVueGeneratedJs(resolved);
229
+ const generatedDts = buildVueGeneratedDts(resolved);
230
+ await writeFile(join(root, 'components', 'generated.js'), generatedJs, 'utf8');
231
+ await writeFile(join(root, 'components', 'generated.d.ts'), generatedDts, 'utf8');
232
+ // 显式导出,减少 TS/IDE 在 re-export 场景下的缓存/解析不一致问题
233
+ const exportedNames = Array.from(new Set(resolved.map((r) => r.componentExportName))).sort((a, b) => a.localeCompare(b));
234
+ const indexDts = `export { ${exportedNames.join(', ')} } from './generated';\n`;
235
+ await writeFile(join(root, 'components', 'index.d.ts'), indexDts, 'utf8');
236
+ console.log('已生成 Vue 组件入口: proto-ui/components/generated.js');
237
+ return;
238
+ }
239
+ console.log('说明: 当前 CLI 已支持 Vue 方向的组件入口生成(后续可扩展 React)。');
240
+ }
241
+ async function main() {
242
+ const version = await readPackageVersion();
243
+ const { values, positionals } = parseArgs({
244
+ allowPositionals: true,
245
+ options: {
246
+ help: { type: 'boolean', short: 'h' },
247
+ version: { type: 'boolean', short: 'v' },
248
+ },
249
+ });
250
+ if (values.help) {
251
+ printHelp(version);
252
+ return;
253
+ }
254
+ if (values.version) {
255
+ console.log(version);
256
+ return;
257
+ }
258
+ const [cmd, ...rest] = positionals;
259
+ if (!cmd || cmd === 'help') {
260
+ printHelp(version);
261
+ return;
262
+ }
263
+ if (cmd === 'version') {
264
+ console.log(version);
265
+ return;
266
+ }
267
+ const cwd = process.cwd();
268
+ if (cmd === 'init') {
269
+ await cmdInit(cwd);
270
+ return;
271
+ }
272
+ if (cmd === 'add') {
273
+ const [adapter, prototype] = rest;
274
+ if (!adapter || !prototype) {
275
+ console.error('用法: npx @proto-ui/cli add <adapter> <prototype>');
276
+ console.error('示例: npx @proto-ui/cli add react shadcn-button');
277
+ process.exitCode = 1;
278
+ return;
279
+ }
280
+ await cmdAdd(cwd, adapter, prototype);
281
+ return;
282
+ }
283
+ console.error(`未知命令: ${cmd}`);
284
+ printHelp(version);
285
+ process.exitCode = 1;
286
+ }
287
+ main().catch((err) => {
288
+ console.error(err);
289
+ process.exitCode = 1;
290
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@proto.ui/cli",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "bin": {
7
+ "proto-ui": "./bin/proto-ui.mjs"
8
+ },
9
+ "dependencies": {
10
+ "typescript": "^5.0.0"
11
+ },
12
+ "description": "Proto UI command line helpers for generating Tailwind token sources and theme css files.",
13
+ "license": "MIT",
14
+ "homepage": "https://github.com/guangliang2019/Prototype-UI/tree/main/packages/cli",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/guangliang2019/Prototype-UI.git",
18
+ "directory": "packages/cli"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/guangliang2019/Prototype-UI/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "keywords": [
27
+ "proto-ui",
28
+ "cli",
29
+ "tailwind",
30
+ "tokens"
31
+ ]
32
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,791 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import ts from 'typescript';
5
+
6
+ const DEFAULT_THEME_NAME = 'shadcn';
7
+ const DEFAULT_THEME_IMPORT = './shadcn-theme.css';
8
+ const DEFAULT_TOKENS_IMPORT = './prototype-tokens.generated.css';
9
+
10
+ const SHADCN_THEME_CSS = `:root {
11
+ --radius: 0.625rem;
12
+ --background: lab(100% 0 0);
13
+ --foreground: lab(2.75381% 0 0);
14
+ --card: lab(100% 0 0);
15
+ --card-foreground: lab(2.75381% 0 0);
16
+ --popover: lab(100% 0 0);
17
+ --popover-foreground: lab(2.75381% 0 0);
18
+ --primary: lab(7.78201% -0.0000149012 0);
19
+ --primary-foreground: lab(98.26% 0 0);
20
+ --secondary: lab(96.52% -0.0000298023 0.0000119209);
21
+ --secondary-foreground: lab(7.78201% -0.0000149012 0);
22
+ --muted: lab(96.52% -0.0000298023 0.0000119209);
23
+ --muted-foreground: lab(48.496% 0 0);
24
+ --accent: lab(96.52% -0.0000298023 0.0000119209);
25
+ --accent-foreground: lab(7.78201% -0.0000149012 0);
26
+ --destructive: lab(48.4493% 77.4328 61.5452);
27
+ --destructive-foreground: lab(96.4152% 3.22586 1.14673);
28
+ --border: lab(90.952% 0 -0.0000119209);
29
+ --input: lab(90.952% 0 -0.0000119209);
30
+ --ring: lab(66.128% -0.0000298023 0.0000119209);
31
+ --chart-1: var(--color-blue-300);
32
+ --chart-2: var(--color-blue-500);
33
+ --chart-3: var(--color-blue-600);
34
+ --chart-4: var(--color-blue-700);
35
+ --chart-5: var(--color-blue-800);
36
+ --sidebar: lab(98.26% 0 0);
37
+ --sidebar-foreground: lab(2.75381% 0 0);
38
+ --sidebar-primary: lab(7.78201% -0.0000149012 0);
39
+ --sidebar-primary-foreground: lab(98.26% 0 0);
40
+ --sidebar-accent: lab(96.52% -0.0000298023 0.0000119209);
41
+ --sidebar-accent-foreground: lab(7.78201% -0.0000149012 0);
42
+ --sidebar-border: lab(90.952% 0 -0.0000119209);
43
+ --sidebar-ring: lab(66.128% -0.0000298023 0.0000119209);
44
+ --surface: lab(97.68% -0.0000298023 0.0000119209);
45
+ --surface-foreground: var(--foreground);
46
+ --code: var(--surface);
47
+ --code-foreground: var(--surface-foreground);
48
+ --code-highlight: lab(95.36% 0 0);
49
+ --code-number: lab(48.96% 0 0);
50
+ --selection: lab(2.75381% 0 0);
51
+ --selection-foreground: lab(100% 0 0);
52
+ }
53
+
54
+ :root.dark,
55
+ :root[data-theme='dark'] {
56
+ --background: lab(2.75381% 0 0);
57
+ --foreground: lab(98.26% 0 0);
58
+ --card: lab(7.78201% -0.0000149012 0);
59
+ --card-foreground: lab(98.26% 0 0);
60
+ --popover: lab(7.78201% -0.0000149012 0);
61
+ --popover-foreground: lab(98.26% 0 0);
62
+ --primary: lab(90.952% 0 -0.0000119209);
63
+ --primary-foreground: lab(7.78201% -0.0000149012 0);
64
+ --secondary: lab(15.204% 0 -0.00000596046);
65
+ --secondary-foreground: lab(98.26% 0 0);
66
+ --muted: lab(15.204% 0 -0.00000596046);
67
+ --muted-foreground: lab(66.128% -0.0000298023 0.0000119209);
68
+ --accent: lab(27.036% 0 0);
69
+ --accent-foreground: lab(98.26% 0 0);
70
+ --destructive: lab(63.7053% 60.745 31.3109);
71
+ --destructive-foreground: lab(49.0747% 69.3434 49.6251);
72
+ --border: lab(100% 0 0 / 0.1);
73
+ --input: lab(100% 0 0 / 0.15);
74
+ --ring: lab(48.496% 0 0);
75
+ --chart-1: var(--color-blue-300);
76
+ --chart-2: var(--color-blue-500);
77
+ --chart-3: var(--color-blue-600);
78
+ --chart-4: var(--color-blue-700);
79
+ --chart-5: var(--color-blue-800);
80
+ --sidebar: lab(7.78201% -0.0000149012 0);
81
+ --sidebar-foreground: lab(98.26% 0 0);
82
+ --sidebar-primary: lab(36.9089% 35.0961 -85.6872);
83
+ --sidebar-primary-foreground: lab(98.26% 0 0);
84
+ --sidebar-accent: lab(15.204% 0 -0.00000596046);
85
+ --sidebar-accent-foreground: lab(98.26% 0 0);
86
+ --sidebar-border: lab(100% 0 0 / 0.1);
87
+ --sidebar-ring: lab(34.924% 0 0);
88
+ --surface: lab(7.22637% -0.0000149012 0);
89
+ --surface-foreground: lab(66.128% -0.0000298023 0.0000119209);
90
+ --code: var(--surface);
91
+ --code-foreground: var(--surface-foreground);
92
+ --code-highlight: lab(15.32% 0 0);
93
+ --code-number: lab(67.52% -0.0000298023 0);
94
+ --selection: lab(90.952% 0 -0.0000119209);
95
+ --selection-foreground: lab(7.78201% -0.0000149012 0);
96
+ }
97
+ `;
98
+
99
+ const TAILWIND_BASE_TEMPLATE = `@import 'tailwindcss';
100
+ @import '__THEME_IMPORT__';
101
+ @import '__TOKENS_IMPORT__';
102
+
103
+ @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
104
+
105
+ @theme {
106
+ --radius: 0.625rem;
107
+ --radius-xl: calc(var(--radius) + 4px);
108
+ --radius-lg: var(--radius);
109
+ --radius-md: calc(var(--radius) - 2px);
110
+ --radius-sm: calc(var(--radius) - 4px);
111
+ --color-background: var(--background);
112
+ --color-foreground: var(--foreground);
113
+ --color-card: var(--card);
114
+ --color-card-foreground: var(--card-foreground);
115
+ --color-popover: var(--popover);
116
+ --color-popover-foreground: var(--popover-foreground);
117
+ --color-primary: var(--primary);
118
+ --color-primary-foreground: var(--primary-foreground);
119
+ --color-secondary: var(--secondary);
120
+ --color-secondary-foreground: var(--secondary-foreground);
121
+ --color-muted: var(--muted);
122
+ --color-muted-foreground: var(--muted-foreground);
123
+ --color-accent: var(--accent);
124
+ --color-accent-foreground: var(--accent-foreground);
125
+ --color-destructive: var(--destructive);
126
+ --color-destructive-foreground: var(--destructive-foreground);
127
+ --color-border: var(--border);
128
+ --color-input: var(--input);
129
+ --color-ring: var(--ring);
130
+ --color-chart-1: var(--chart-1);
131
+ --color-chart-2: var(--chart-2);
132
+ --color-chart-3: var(--chart-3);
133
+ --color-chart-4: var(--chart-4);
134
+ --color-chart-5: var(--chart-5);
135
+ --color-sidebar: var(--sidebar);
136
+ --color-sidebar-foreground: var(--sidebar-foreground);
137
+ --color-sidebar-primary: var(--sidebar-primary);
138
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
139
+ --color-sidebar-accent: var(--sidebar-accent);
140
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
141
+ --color-sidebar-border: var(--sidebar-border);
142
+ --color-sidebar-ring: var(--sidebar-ring);
143
+ --color-surface: var(--surface);
144
+ --color-surface-foreground: var(--surface-foreground);
145
+ --color-code: var(--code);
146
+ --color-code-foreground: var(--code-foreground);
147
+ --color-code-highlight: var(--code-highlight);
148
+ --color-code-number: var(--code-number);
149
+ --color-selection: var(--selection);
150
+ --color-selection-foreground: var(--selection-foreground);
151
+ }
152
+
153
+ @layer base {
154
+ :root {
155
+ --radius-xl: calc(var(--radius) + 4px);
156
+ --radius-lg: var(--radius);
157
+ --radius-md: calc(var(--radius) - 2px);
158
+ --radius-sm: calc(var(--radius) - 4px);
159
+ }
160
+
161
+ * {
162
+ border-color: var(--color-border);
163
+ }
164
+ }
165
+
166
+ body {
167
+ background-color: var(--color-background);
168
+ color: var(--color-foreground);
169
+ }
170
+ `;
171
+
172
+ export async function run(argv) {
173
+ const [command, ...rest] = argv;
174
+
175
+ if (!command || command === '--help' || command === '-h') {
176
+ printHelp();
177
+ return;
178
+ }
179
+
180
+ if (command === 'tokens') {
181
+ await runGenerateTokens(rest);
182
+ return;
183
+ }
184
+
185
+ if (command === 'tailwindcss') {
186
+ await runGenerateTailwindCss(rest);
187
+ return;
188
+ }
189
+
190
+ if (command === 'theme') {
191
+ await runGenerateTheme(rest);
192
+ return;
193
+ }
194
+
195
+ await runPreset(command, rest);
196
+ }
197
+
198
+ function printHelp() {
199
+ console.log(`proto-ui
200
+
201
+ Usage:
202
+ proto-ui <theme> [--prototypes <dir>] [--styles-dir <dir>]
203
+ proto-ui tokens --input <dir> --out <file>
204
+ proto-ui tailwindcss [--theme-import <path>] [--tokens-import <path>] --out <file>
205
+ proto-ui theme <name> --out <file>
206
+
207
+ Examples:
208
+ proto-ui shadcn --prototypes ./src/prototypes --styles-dir ./src/styles
209
+ proto-ui tokens --input ./packages/prototypes --out ./src/styles/prototype-tokens.generated.css
210
+ proto-ui tailwindcss --out ./src/styles/tailwindcss.css
211
+ proto-ui theme shadcn --out ./src/styles/shadcn-theme.css
212
+ `);
213
+ }
214
+
215
+ async function runPreset(themeName, args) {
216
+ const options = parseOptions(args);
217
+ const prototypesDir = options.prototypes ?? './src/prototypes';
218
+ const stylesDir = options['styles-dir'] ?? './src/styles';
219
+ const tokensFileName = options['tokens-file'] ?? 'prototype-tokens.generated.css';
220
+ const tailwindFileName = options['tailwind-file'] ?? 'tailwindcss.css';
221
+ const themeFileName = options['theme-file'] ?? `${themeName}-theme.css`;
222
+
223
+ const tokensOut = path.join(stylesDir, tokensFileName);
224
+ const themeOut = path.join(stylesDir, themeFileName);
225
+ const tailwindOut = path.join(stylesDir, tailwindFileName);
226
+
227
+ await runGenerateTokens(['--input', prototypesDir, '--out', tokensOut]);
228
+ await runGenerateTheme([themeName, '--out', themeOut]);
229
+
230
+ const tailwindAbs = path.resolve(process.cwd(), tailwindOut);
231
+ const themeImport = toCssImportPath(tailwindAbs, path.resolve(process.cwd(), themeOut));
232
+ const tokensImport = toCssImportPath(tailwindAbs, path.resolve(process.cwd(), tokensOut));
233
+
234
+ await runGenerateTailwindCss([
235
+ '--out',
236
+ tailwindOut,
237
+ '--theme-import',
238
+ themeImport,
239
+ '--tokens-import',
240
+ tokensImport,
241
+ ]);
242
+
243
+ console.log(
244
+ `[proto-ui] setup(${themeName}): completed tokens + theme + tailwindcss in ${stylesDir}`
245
+ );
246
+ }
247
+
248
+ async function runGenerateTokens(args) {
249
+ const options = parseOptions(args);
250
+ const input = requiredOption(options, 'input');
251
+ const outFile = requiredOption(options, 'out');
252
+ const root = path.resolve(process.cwd(), input);
253
+ const outputFile = path.resolve(process.cwd(), outFile);
254
+
255
+ const files = await collectTsFiles(root);
256
+ const tokens = new Set();
257
+
258
+ for (const file of files) {
259
+ const sourceText = await fs.readFile(file, 'utf8');
260
+ const sourceFile = ts.createSourceFile(
261
+ file,
262
+ sourceText,
263
+ ts.ScriptTarget.Latest,
264
+ true,
265
+ file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
266
+ );
267
+ const scope = createScope();
268
+ walk(sourceFile, scope, tokens);
269
+ }
270
+
271
+ const css = renderTokenCss(Array.from(tokens).sort());
272
+ await ensureDirectory(outputFile);
273
+ await fs.writeFile(outputFile, css, 'utf8');
274
+ console.log(`[proto-ui] tokens: wrote ${tokens.size} tokens -> ${relativeToCwd(outputFile)}`);
275
+ }
276
+
277
+ async function runGenerateTailwindCss(args) {
278
+ const options = parseOptions(args);
279
+ const outFile = requiredOption(options, 'out');
280
+ const themeImport = options['theme-import'] ?? DEFAULT_THEME_IMPORT;
281
+ const tokensImport = options['tokens-import'] ?? DEFAULT_TOKENS_IMPORT;
282
+ const outputFile = path.resolve(process.cwd(), outFile);
283
+ const css = TAILWIND_BASE_TEMPLATE.replace('__THEME_IMPORT__', themeImport).replace(
284
+ '__TOKENS_IMPORT__',
285
+ tokensImport
286
+ );
287
+
288
+ await ensureDirectory(outputFile);
289
+ await fs.writeFile(outputFile, css, 'utf8');
290
+ console.log(`[proto-ui] tailwindcss: wrote ${relativeToCwd(outputFile)}`);
291
+ }
292
+
293
+ async function runGenerateTheme(args) {
294
+ const [name, ...rest] = args;
295
+ if (!name || name.startsWith('-')) {
296
+ throw new Error(
297
+ 'theme name is required. Example: proto-ui theme shadcn --out ./src/styles/shadcn-theme.css'
298
+ );
299
+ }
300
+
301
+ const options = parseOptions(rest);
302
+ const outFile = requiredOption(options, 'out');
303
+ const outputFile = path.resolve(process.cwd(), outFile);
304
+ const normalizedName = name.toLowerCase();
305
+
306
+ let css = '';
307
+ if (normalizedName === DEFAULT_THEME_NAME) {
308
+ css = SHADCN_THEME_CSS;
309
+ } else {
310
+ throw new Error(`unsupported theme "${name}". currently supported: ${DEFAULT_THEME_NAME}.`);
311
+ }
312
+
313
+ await ensureDirectory(outputFile);
314
+ await fs.writeFile(outputFile, css, 'utf8');
315
+ console.log(`[proto-ui] theme(${normalizedName}): wrote ${relativeToCwd(outputFile)}`);
316
+ }
317
+
318
+ function parseOptions(args) {
319
+ const options = {};
320
+ for (let i = 0; i < args.length; i += 1) {
321
+ const token = args[i];
322
+ if (!token.startsWith('--')) continue;
323
+ const key = token.slice(2);
324
+ const next = args[i + 1];
325
+ if (!next || next.startsWith('--')) {
326
+ options[key] = 'true';
327
+ continue;
328
+ }
329
+ options[key] = next;
330
+ i += 1;
331
+ }
332
+ return options;
333
+ }
334
+
335
+ function requiredOption(options, key) {
336
+ const value = options[key];
337
+ if (!value) throw new Error(`missing required option: --${key}`);
338
+ return value;
339
+ }
340
+
341
+ async function ensureDirectory(filePath) {
342
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
343
+ }
344
+
345
+ function relativeToCwd(filePath) {
346
+ return path.relative(process.cwd(), filePath) || '.';
347
+ }
348
+
349
+ function toCssImportPath(fromFile, toFile) {
350
+ const fromDir = path.dirname(fromFile);
351
+ const relative = path.relative(fromDir, toFile).replace(/\\/g, '/');
352
+ if (relative.startsWith('.')) return relative;
353
+ return `./${relative}`;
354
+ }
355
+
356
+ function createScope(parent = null) {
357
+ return { parent, bindings: new Map() };
358
+ }
359
+
360
+ function walk(node, scope, tokens) {
361
+ if (createsScope(node)) {
362
+ const nextScope = createScope(scope);
363
+
364
+ if (hasStatements(node)) {
365
+ for (const stmt of node.statements) {
366
+ if (ts.isVariableStatement(stmt)) {
367
+ for (const decl of stmt.declarationList.declarations) {
368
+ registerDeclaration(decl, nextScope);
369
+ if (decl.initializer) walk(decl.initializer, nextScope, tokens);
370
+ }
371
+ continue;
372
+ }
373
+ walk(stmt, nextScope, tokens);
374
+ }
375
+ return;
376
+ }
377
+
378
+ ts.forEachChild(node, (child) => walk(child, nextScope, tokens));
379
+ return;
380
+ }
381
+
382
+ if (
383
+ ts.isCallExpression(node) &&
384
+ ts.isIdentifier(node.expression) &&
385
+ node.expression.text === 'tw'
386
+ ) {
387
+ for (const arg of node.arguments) {
388
+ const value = resolveExpression(arg, scope);
389
+ for (const token of value.strings.flatMap(splitTokens)) {
390
+ tokens.add(token);
391
+ }
392
+ }
393
+ }
394
+
395
+ if (ts.isCallExpression(node) && isPropertyNamed(node.expression, 'rule')) {
396
+ collectRuleVariantTokens(node, scope, tokens);
397
+ }
398
+
399
+ ts.forEachChild(node, (child) => walk(child, scope, tokens));
400
+ }
401
+
402
+ function createsScope(node) {
403
+ return (
404
+ ts.isSourceFile(node) ||
405
+ ts.isBlock(node) ||
406
+ ts.isModuleBlock(node) ||
407
+ ts.isCaseBlock(node) ||
408
+ ts.isFunctionDeclaration(node) ||
409
+ ts.isFunctionExpression(node) ||
410
+ ts.isArrowFunction(node)
411
+ );
412
+ }
413
+
414
+ function hasStatements(node) {
415
+ return (
416
+ ts.isSourceFile(node) || ts.isBlock(node) || ts.isModuleBlock(node) || ts.isCaseBlock(node)
417
+ );
418
+ }
419
+
420
+ function registerDeclaration(decl, scope) {
421
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) return;
422
+ scope.bindings.set(decl.name.text, resolveBinding(decl.initializer, scope));
423
+ }
424
+
425
+ function resolveExpression(node, scope) {
426
+ if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
427
+ return asStringValue([node.text]);
428
+ }
429
+
430
+ if (ts.isArrayLiteralExpression(node)) {
431
+ const parts = [];
432
+ for (const element of node.elements) {
433
+ const value = resolveExpression(element, scope);
434
+ if (!value.single) return emptyValue();
435
+ parts.push(value.single);
436
+ }
437
+ return asStringValue([parts.join(',')]);
438
+ }
439
+
440
+ if (
441
+ ts.isCallExpression(node) &&
442
+ ts.isPropertyAccessExpression(node.expression) &&
443
+ node.expression.name.text === 'join'
444
+ ) {
445
+ return resolveJoinCall(node, scope);
446
+ }
447
+
448
+ if (ts.isIdentifier(node)) {
449
+ return lookup(node.text, scope);
450
+ }
451
+
452
+ if (
453
+ ts.isParenthesizedExpression(node) ||
454
+ ts.isAsExpression(node) ||
455
+ ts.isTypeAssertionExpression(node)
456
+ ) {
457
+ return resolveExpression(node.expression, scope);
458
+ }
459
+
460
+ if (ts.isObjectLiteralExpression(node)) {
461
+ const entries = new Map();
462
+ for (const prop of node.properties) {
463
+ if (ts.isPropertyAssignment(prop)) {
464
+ const key = getPropertyName(prop.name);
465
+ if (!key) continue;
466
+ const value = resolveExpression(prop.initializer, scope);
467
+ if (value.strings.length > 0) entries.set(key, value.strings);
468
+ } else if (ts.isShorthandPropertyAssignment(prop)) {
469
+ const value = lookup(prop.name.text, scope);
470
+ if (value.strings.length > 0) entries.set(prop.name.text, value.strings);
471
+ }
472
+ }
473
+ return asMapValue(entries);
474
+ }
475
+
476
+ if (ts.isElementAccessExpression(node)) {
477
+ const base = resolveExpression(node.expression, scope);
478
+ if (!base.map) return emptyValue();
479
+
480
+ if (node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression)) {
481
+ return asStringValue(base.map.get(node.argumentExpression.text) ?? []);
482
+ }
483
+
484
+ const out = new Set();
485
+ for (const strings of base.map.values()) {
486
+ for (const value of strings) out.add(value);
487
+ }
488
+ return asStringValue(Array.from(out));
489
+ }
490
+
491
+ if (ts.isConditionalExpression(node)) {
492
+ const values = new Set([
493
+ ...resolveExpression(node.whenTrue, scope).strings,
494
+ ...resolveExpression(node.whenFalse, scope).strings,
495
+ ]);
496
+ return asStringValue(Array.from(values));
497
+ }
498
+
499
+ return emptyValue();
500
+ }
501
+
502
+ function resolveJoinCall(node, scope) {
503
+ const separatorArg = node.arguments[0];
504
+ const separator =
505
+ separatorArg &&
506
+ (ts.isStringLiteralLike(separatorArg) || ts.isNoSubstitutionTemplateLiteral(separatorArg))
507
+ ? separatorArg.text
508
+ : ',';
509
+ const base = resolveExpression(node.expression.expression, scope);
510
+ if (!base.single) return emptyValue();
511
+ return asStringValue([base.single.split(',').join(separator)]);
512
+ }
513
+
514
+ function lookup(name, scope) {
515
+ let current = scope;
516
+ while (current) {
517
+ const value = current.bindings.get(name);
518
+ if (value) return value;
519
+ current = current.parent;
520
+ }
521
+ return emptyValue();
522
+ }
523
+
524
+ function resolveBinding(node, scope) {
525
+ const semantic = resolveSemanticBinding(node);
526
+ const value = resolveExpression(node, scope);
527
+ return semantic ? { ...value, semantic } : value;
528
+ }
529
+
530
+ function resolveSemanticBinding(node) {
531
+ if (
532
+ !ts.isCallExpression(node) ||
533
+ !isPropertyAccessChain(node.expression, ['state', 'fromInteraction'])
534
+ ) {
535
+ if (
536
+ !ts.isCallExpression(node) ||
537
+ !isPropertyAccessChain(node.expression, ['state', 'fromAccessibility'])
538
+ ) {
539
+ return null;
540
+ }
541
+ }
542
+
543
+ const kind = node.expression.name.text === 'fromInteraction' ? 'interaction' : 'accessibility';
544
+ const firstArg = node.arguments[0];
545
+ if (!firstArg || !ts.isStringLiteralLike(firstArg)) return null;
546
+ const name = firstArg.text;
547
+
548
+ if (kind === 'interaction') {
549
+ return (
550
+ {
551
+ hovered: 'hover',
552
+ pressed: 'active',
553
+ disabled: 'data-[disabled]',
554
+ focused: 'data-[focused]',
555
+ focusVisible: 'data-[focus-visible]',
556
+ }[name] ?? null
557
+ );
558
+ }
559
+
560
+ return (
561
+ {
562
+ expanded: 'aria-expanded',
563
+ invalid: 'aria-invalid',
564
+ selected: 'aria-selected',
565
+ checked: 'aria-checked',
566
+ current: 'aria-current',
567
+ }[name] ?? null
568
+ );
569
+ }
570
+
571
+ function collectRuleVariantTokens(node, scope, tokens) {
572
+ const config = node.arguments[0];
573
+ if (!config || !ts.isObjectLiteralExpression(config)) return;
574
+
575
+ const whenProp = config.properties.find(
576
+ (prop) => ts.isPropertyAssignment(prop) && getPropertyName(prop.name) === 'when'
577
+ );
578
+ const intentProp = config.properties.find(
579
+ (prop) => ts.isPropertyAssignment(prop) && getPropertyName(prop.name) === 'intent'
580
+ );
581
+ if (
582
+ !whenProp ||
583
+ !intentProp ||
584
+ !ts.isPropertyAssignment(whenProp) ||
585
+ !ts.isPropertyAssignment(intentProp)
586
+ ) {
587
+ return;
588
+ }
589
+
590
+ const variants = analyzeWhenVariants(whenProp.initializer, scope);
591
+ if (variants.length === 0) return;
592
+
593
+ const intentTokens = collectTwTokens(intentProp.initializer, scope);
594
+ for (const token of intentTokens) {
595
+ tokens.add(`${variants.join(':')}:${token}`);
596
+ }
597
+ }
598
+
599
+ function analyzeWhenVariants(node, scope) {
600
+ const out = new Set();
601
+
602
+ visit(node);
603
+ return Array.from(out).sort(compareVariants);
604
+
605
+ function visit(current) {
606
+ if (ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
607
+ visit(current.body);
608
+ return;
609
+ }
610
+
611
+ if (
612
+ ts.isParenthesizedExpression(current) ||
613
+ ts.isAsExpression(current) ||
614
+ ts.isTypeAssertionExpression(current)
615
+ ) {
616
+ visit(current.expression);
617
+ return;
618
+ }
619
+
620
+ if (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
621
+ const method = current.expression.name.text;
622
+
623
+ if (method === 'all' || method === 'any') {
624
+ for (const arg of current.arguments) visit(arg);
625
+ return;
626
+ }
627
+
628
+ if (method === 'eq') {
629
+ const subject = current.expression.expression;
630
+ if (ts.isCallExpression(subject) && ts.isPropertyAccessExpression(subject.expression)) {
631
+ const subjectMethod = subject.expression.name.text;
632
+ if (subjectMethod === 'state') {
633
+ const firstArg = subject.arguments[0];
634
+ if (firstArg && ts.isIdentifier(firstArg)) {
635
+ const binding = lookup(firstArg.text, scope);
636
+ if (binding.semantic) out.add(binding.semantic);
637
+ }
638
+ return;
639
+ }
640
+
641
+ if (subjectMethod === 'meta') {
642
+ const key = subject.arguments[0];
643
+ const value = current.arguments[0];
644
+ if (
645
+ key &&
646
+ value &&
647
+ ts.isStringLiteralLike(key) &&
648
+ ts.isStringLiteralLike(value) &&
649
+ key.text === 'colorScheme' &&
650
+ value.text === 'dark'
651
+ ) {
652
+ out.add('dark');
653
+ }
654
+ }
655
+ }
656
+ }
657
+ }
658
+
659
+ ts.forEachChild(current, visit);
660
+ }
661
+ }
662
+
663
+ function collectTwTokens(node, scope) {
664
+ const found = new Set();
665
+
666
+ visit(node, scope);
667
+ return Array.from(found);
668
+
669
+ function visit(current, currentScope) {
670
+ if (createsScope(current)) {
671
+ const nextScope = createScope(currentScope);
672
+ if (hasStatements(current)) {
673
+ for (const stmt of current.statements) {
674
+ if (ts.isVariableStatement(stmt)) {
675
+ for (const decl of stmt.declarationList.declarations) {
676
+ registerDeclaration(decl, nextScope);
677
+ if (decl.initializer) visit(decl.initializer, nextScope);
678
+ }
679
+ continue;
680
+ }
681
+ visit(stmt, nextScope);
682
+ }
683
+ return;
684
+ }
685
+ }
686
+
687
+ if (
688
+ ts.isCallExpression(current) &&
689
+ ts.isIdentifier(current.expression) &&
690
+ current.expression.text === 'tw'
691
+ ) {
692
+ for (const arg of current.arguments) {
693
+ const value = resolveExpression(arg, currentScope);
694
+ for (const token of value.strings.flatMap(splitTokens)) found.add(token);
695
+ }
696
+ }
697
+
698
+ ts.forEachChild(current, (child) => visit(child, currentScope));
699
+ }
700
+ }
701
+
702
+ function compareVariants(a, b) {
703
+ const order = ['dark', 'hover', 'active', 'focus', 'focus-visible', 'disabled'];
704
+ const ai = order.indexOf(a);
705
+ const bi = order.indexOf(b);
706
+ if (ai !== -1 || bi !== -1) return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
707
+ return a.localeCompare(b);
708
+ }
709
+
710
+ function isPropertyNamed(node, name) {
711
+ return ts.isPropertyAccessExpression(node) && node.name.text === name;
712
+ }
713
+
714
+ function isPropertyAccessChain(node, names) {
715
+ let current = node;
716
+ for (let i = names.length - 1; i >= 0; i -= 1) {
717
+ if (!ts.isPropertyAccessExpression(current) || current.name.text !== names[i]) return false;
718
+ current = current.expression;
719
+ }
720
+ return ts.isIdentifier(current);
721
+ }
722
+
723
+ function getPropertyName(name) {
724
+ if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text;
725
+ return null;
726
+ }
727
+
728
+ function splitTokens(value) {
729
+ return value
730
+ .split(/\s+/)
731
+ .map((token) => token.trim())
732
+ .filter(Boolean);
733
+ }
734
+
735
+ function emptyValue() {
736
+ return { strings: [], single: null, map: null, semantic: null };
737
+ }
738
+
739
+ function asStringValue(strings) {
740
+ return {
741
+ strings,
742
+ single: strings.length === 1 ? strings[0] : null,
743
+ map: null,
744
+ semantic: null,
745
+ };
746
+ }
747
+
748
+ function asMapValue(map) {
749
+ const strings = [];
750
+ for (const values of map.values()) strings.push(...values);
751
+ return {
752
+ strings,
753
+ single: null,
754
+ map,
755
+ semantic: null,
756
+ };
757
+ }
758
+
759
+ function renderTokenCss(tokens) {
760
+ const lines = [
761
+ '/* This file is auto-generated by @proto.ui/cli (proto-ui tokens). */',
762
+ '/* Do not edit by hand. */',
763
+ '',
764
+ ];
765
+
766
+ for (const token of tokens) {
767
+ lines.push(`@source inline("${escapeForCss(token)}");`);
768
+ }
769
+
770
+ lines.push('');
771
+ return lines.join('\n');
772
+ }
773
+
774
+ function escapeForCss(token) {
775
+ return token.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
776
+ }
777
+
778
+ async function collectTsFiles(dir) {
779
+ const out = [];
780
+ const entries = await fs.readdir(dir, { withFileTypes: true });
781
+ for (const entry of entries) {
782
+ const fullPath = path.join(dir, entry.name);
783
+ if (entry.isDirectory()) {
784
+ if (entry.name === 'dist' || entry.name === 'test' || entry.name === 'node_modules') continue;
785
+ out.push(...(await collectTsFiles(fullPath)));
786
+ continue;
787
+ }
788
+ if (entry.isFile() && /\.(ts|tsx|mts|cts)$/.test(entry.name)) out.push(fullPath);
789
+ }
790
+ return out;
791
+ }