@lemmo-lab/tokens 2.1.0 → 2.1.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.
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { printBanner } from '../src/utils/banner.js';
4
+ import { commandList } from '../src/commands/list.js';
5
+ import { commandInstall } from '../src/commands/install.js';
6
+ import { commandAddTheme } from '../src/commands/add-theme.js';
7
+ import { commandDiff } from '../src/commands/diff.js';
8
+
9
+ function printHelp() {
10
+ printBanner();
11
+ console.log(`Usage:
12
+ tokens <command> [options]
13
+ npx @lemmo-lab/tokens-cli <command> [options]
14
+
15
+ Commands:
16
+ install, init, pull Install token styles & themes (Interactive or Flags)
17
+ list, ls List available themes, formats, and token scales
18
+ add-theme <name> Scaffold a new theme adhering to Tier 2 contract
19
+ diff <theme1> <theme2> Compare two theme definitions and display differences
20
+ help Show this help message
21
+
22
+ Options for 'install':
23
+ --interactive, -i Launch the interactive installation wizard
24
+ --theme, -t <names> Target theme(s): default, light, neon, midnight, all
25
+ --format, -f <formats> Output format(s): css, tailwind, js, ts, all
26
+ --out, -o <dir> Destination directory [default: ./tokens]
27
+ --yes, -y Accept all default configuration without prompting
28
+ --dry-run Preview files to be installed without writing
29
+
30
+ Examples:
31
+ # Interactive wizard (Recommended for new projects):
32
+ tokens install
33
+
34
+ # Install only light theme CSS to ./src/styles/tokens:
35
+ tokens install --theme light --out ./src/styles/tokens
36
+
37
+ # Install default & neon themes with Tailwind preset:
38
+ tokens install --theme default,neon --format css,tailwind --out ./tokens
39
+
40
+ # Compare dark (default) and light themes:
41
+ tokens diff default light
42
+ `);
43
+ }
44
+
45
+ function parseArgs(rawArgs) {
46
+ const parsed = {
47
+ command: null,
48
+ target: null,
49
+ target2: null,
50
+ theme: null,
51
+ format: null,
52
+ out: null,
53
+ interactive: false,
54
+ yes: false,
55
+ dryRun: false
56
+ };
57
+
58
+ let i = 0;
59
+ while (i < rawArgs.length) {
60
+ const arg = rawArgs[i];
61
+ if (!arg.startsWith('-')) {
62
+ if (!parsed.command) parsed.command = arg;
63
+ else if (!parsed.target) parsed.target = arg;
64
+ else if (!parsed.target2) parsed.target2 = arg;
65
+ } else if (arg === '--help' || arg === '-h') {
66
+ parsed.command = 'help';
67
+ } else if (arg === '--theme' || arg === '-t') {
68
+ parsed.theme = rawArgs[++i];
69
+ } else if (arg === '--format' || arg === '-f') {
70
+ parsed.format = rawArgs[++i];
71
+ } else if (arg === '--out' || arg === '-o') {
72
+ parsed.out = rawArgs[++i];
73
+ } else if (arg === '--interactive' || arg === '-i') {
74
+ parsed.interactive = true;
75
+ } else if (arg === '--yes' || arg === '-y') {
76
+ parsed.yes = true;
77
+ } else if (arg === '--dry-run') {
78
+ parsed.dryRun = true;
79
+ }
80
+ i++;
81
+ }
82
+ return parsed;
83
+ }
84
+
85
+ async function main() {
86
+ const args = process.argv.slice(2);
87
+ const options = parseArgs(args);
88
+
89
+ switch (options.command) {
90
+ case 'list':
91
+ case 'ls':
92
+ commandList();
93
+ break;
94
+ case 'install':
95
+ case 'init':
96
+ case 'pull':
97
+ await commandInstall(options);
98
+ break;
99
+ case 'add-theme':
100
+ await commandAddTheme(options.target);
101
+ break;
102
+ case 'diff':
103
+ commandDiff(options.target, options.target2);
104
+ break;
105
+ case 'help':
106
+ case '--help':
107
+ case '-h':
108
+ printHelp();
109
+ break;
110
+ default:
111
+ if (!options.command) {
112
+ // If run with no arguments, launch interactive installation
113
+ await commandInstall({ interactive: true });
114
+ } else {
115
+ printHelp();
116
+ }
117
+ break;
118
+ }
119
+ }
120
+
121
+ main().catch(err => {
122
+ console.error('\nāŒ CLI Error:', err.message);
123
+ process.exit(1);
124
+ });
@@ -0,0 +1,57 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { printBanner } from '../utils/banner.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const CLI_ROOT = path.resolve(__dirname, '../../');
9
+ const REPO_ROOT = path.resolve(CLI_ROOT, '../../');
10
+
11
+ export async function commandAddTheme(themeName) {
12
+ printBanner();
13
+
14
+ if (!themeName) {
15
+ console.error('āŒ Please provide a theme name: `tokens add-theme <theme-name>`');
16
+ process.exit(1);
17
+ }
18
+
19
+ const cleanName = themeName.toLowerCase().replace(/[^a-z0-9_-]/g, '');
20
+ const themesDir = path.join(REPO_ROOT, 'packages/tokens/src/themes');
21
+
22
+ if (!fs.existsSync(themesDir)) {
23
+ console.error('āŒ Must be executed within the tokens monorepo to scaffold themes.');
24
+ process.exit(1);
25
+ }
26
+
27
+ const newThemePath = path.join(themesDir, `${cleanName}.json`);
28
+ if (fs.existsSync(newThemePath)) {
29
+ console.error(`āŒ Theme '${cleanName}' already exists at: ${newThemePath}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ // Load semantic contract template to pre-populate all required contract keys
34
+ const semanticContractPath = path.join(REPO_ROOT, 'packages/tokens/src/semantic/default.json');
35
+ const contract = JSON.parse(fs.readFileSync(semanticContractPath, 'utf8'));
36
+
37
+ // Scaffold theme conforming 100% to governance rules
38
+ const scaffoldTheme = {
39
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
40
+ theme: {
41
+ color: contract.semantic?.color || {},
42
+ gradient: contract.semantic?.gradient || {},
43
+ radius: {
44
+ base: { $type: 'dimension', $value: '0.5rem' }
45
+ }
46
+ }
47
+ };
48
+
49
+ fs.writeFileSync(newThemePath, JSON.stringify(scaffoldTheme, null, 2) + '\n', 'utf8');
50
+
51
+ console.log(`šŸŽ‰ Theme \x1b[32m${cleanName}\x1b[0m scaffolded successfully!`);
52
+ console.log(`šŸ“ File created: \x1b[36m${newThemePath}\x1b[0m`);
53
+ console.log(`\nNext Steps:`);
54
+ console.log(` 1. Open ${newThemePath} and update Tier 1 alias references.`);
55
+ console.log(` 2. Run \x1b[1mpnpm validate\x1b[0m to ensure it passes all 9 governance rules.`);
56
+ console.log(` 3. Run \x1b[1mpnpm build\x1b[0m to regenerate CSS and Tailwind outputs.`);
57
+ }
@@ -0,0 +1,72 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { printBanner } from '../utils/banner.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const CLI_ROOT = path.resolve(__dirname, '../../');
9
+ const REPO_ROOT = path.resolve(CLI_ROOT, '../../');
10
+
11
+ function collectLeafTokens(obj, prefix = '') {
12
+ let res = {};
13
+ for (const [k, v] of Object.entries(obj)) {
14
+ if (k.startsWith('$')) continue;
15
+ const curPath = prefix ? `${prefix}.${k}` : k;
16
+ if (v && typeof v === 'object') {
17
+ if (v.$value !== undefined) {
18
+ res[curPath] = v.$value;
19
+ } else {
20
+ Object.assign(res, collectLeafTokens(v, curPath));
21
+ }
22
+ }
23
+ }
24
+ return res;
25
+ }
26
+
27
+ export function commandDiff(theme1, theme2) {
28
+ printBanner();
29
+
30
+ if (!theme1 || !theme2) {
31
+ console.error('āŒ Usage: `tokens diff <theme1> <theme2>` (e.g. `tokens diff default light`)');
32
+ process.exit(1);
33
+ }
34
+
35
+ const themesDir = path.join(REPO_ROOT, 'packages/tokens/src/themes');
36
+ const t1File = path.join(themesDir, `${theme1.toLowerCase()}.json`);
37
+ const t2File = path.join(themesDir, `${theme2.toLowerCase()}.json`);
38
+
39
+ if (!fs.existsSync(t1File)) {
40
+ console.error(`āŒ Theme '${theme1}' not found at: ${t1File}`);
41
+ process.exit(1);
42
+ }
43
+ if (!fs.existsSync(t2File)) {
44
+ console.error(`āŒ Theme '${theme2}' not found at: ${t2File}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ const t1Data = JSON.parse(fs.readFileSync(t1File, 'utf8'));
49
+ const t2Data = JSON.parse(fs.readFileSync(t2File, 'utf8'));
50
+
51
+ const t1Tokens = collectLeafTokens(t1Data.theme || {});
52
+ const t2Tokens = collectLeafTokens(t2Data.theme || {});
53
+
54
+ const allKeys = Array.from(new Set([...Object.keys(t1Tokens), ...Object.keys(t2Tokens)])).sort();
55
+
56
+ console.log(`šŸ“Š Comparing themes: \x1b[33m${theme1}\x1b[0m vs \x1b[36m${theme2}\x1b[0m\n`);
57
+
58
+ let diffCount = 0;
59
+ for (const k of allKeys) {
60
+ const v1 = t1Tokens[k];
61
+ const v2 = t2Tokens[k];
62
+ if (v1 !== v2) {
63
+ diffCount++;
64
+ console.log(` • \x1b[1m${k}\x1b[0m:`);
65
+ console.log(` \x1b[33m${theme1}\x1b[0m: ${v1 || '<undefined>'}`);
66
+ console.log(` \x1b[36m${theme2}\x1b[0m: ${v2 || '<undefined>'}\n`);
67
+ }
68
+ }
69
+
70
+ console.log(`=============================================================`);
71
+ console.log(`Total differences found: ${diffCount} token(s).`);
72
+ }
@@ -0,0 +1,275 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { printBanner } from '../utils/banner.js';
5
+ import { createPromptSession } from '../utils/prompts.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const CLI_ROOT = path.resolve(__dirname, '../../');
10
+ const REPO_ROOT = path.resolve(CLI_ROOT, '../../');
11
+
12
+ const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/lemmo-lab/tokens/main/packages/tokens-build/dist';
13
+
14
+ const ALL_THEMES = ['default', 'light', 'neon', 'midnight'];
15
+ const ALL_FORMATS = ['css', 'tailwind', 'js', 'ts'];
16
+
17
+ async function getAssetContent(relPath) {
18
+ // 1. Direct dist folder (when bundled inside @lemmo-lab/tokens package)
19
+ const distDirect = path.resolve(__dirname, '../../../');
20
+ if (fs.existsSync(path.join(distDirect, relPath))) {
21
+ return fs.readFileSync(path.join(distDirect, relPath), 'utf8');
22
+ }
23
+
24
+ // 2. Check local assets dir inside CLI package
25
+ const cliAssetPath = path.join(CLI_ROOT, 'assets', relPath);
26
+ if (fs.existsSync(cliAssetPath)) {
27
+ return fs.readFileSync(cliAssetPath, 'utf8');
28
+ }
29
+
30
+ // 3. Check monorepo dist
31
+ const distPath = path.join(REPO_ROOT, 'packages/tokens-build/dist', relPath);
32
+ if (fs.existsSync(distPath)) {
33
+ return fs.readFileSync(distPath, 'utf8');
34
+ }
35
+
36
+ // 3. Fallback to remote GitHub raw
37
+ const remoteUrl = `${GITHUB_RAW_BASE}/${relPath}`;
38
+ try {
39
+ const res = await fetch(remoteUrl);
40
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
41
+ return await res.text();
42
+ } catch (err) {
43
+ throw new Error(`Failed to load asset ${relPath}: ${err.message}`);
44
+ }
45
+ }
46
+
47
+ export async function commandInstall(options) {
48
+ printBanner();
49
+
50
+ let targetOut = options.out || './tokens';
51
+ let targetThemes = [];
52
+ let targetFormats = [];
53
+ let generateRootIndex = true;
54
+ let isDryRun = options.dryRun || false;
55
+
56
+ // Determine if interactive session is needed
57
+ const isInteractive = options.interactive || (!options.theme && !options.format && !options.yes);
58
+
59
+ if (isInteractive) {
60
+ console.log('šŸ”® \x1b[1mInteractive Lemmo Tokens Installation Wizard\x1b[0m\n');
61
+ const prompts = await createPromptSession();
62
+
63
+ try {
64
+ // 1. Output directory
65
+ targetOut = await prompts.askText('šŸ“ Where should the tokens be installed?', targetOut);
66
+
67
+ // 2. Format selection
68
+ targetFormats = await prompts.askMultiChoice(
69
+ 'šŸ“¦ Select the formats you want to install:',
70
+ [
71
+ { label: 'CSS Variables (variables.css & theme overrides)', value: 'css' },
72
+ { label: 'Tailwind CSS Preset (tailwind.preset.js)', value: 'tailwind' },
73
+ { label: 'JavaScript / TypeScript Dictionaries (tokens.js & tokens.d.ts)', value: 'js' }
74
+ ],
75
+ [0] // default CSS
76
+ );
77
+
78
+ // 3. Theme selection
79
+ targetThemes = await prompts.askMultiChoice(
80
+ 'šŸŽØ Select which theme presets to install:',
81
+ [
82
+ { label: 'All themes (Default Dark, Light Day, Neon, Midnight)', value: 'all' },
83
+ { label: 'Default (Dark theme — #131517 canvas)', value: 'default' },
84
+ { label: 'Light (Day theme — #f8f9fa canvas, high contrast lime)', value: 'light' },
85
+ { label: 'Neon (Cyberpunk high contrast — #00ff66 & #00f0ff)', value: 'neon' },
86
+ { label: 'Midnight (OLED Pure Black — #000000)', value: 'midnight' }
87
+ ],
88
+ [0]
89
+ );
90
+
91
+ if (targetThemes.includes('all')) {
92
+ targetThemes = ['default', 'light', 'neon', 'midnight'];
93
+ }
94
+
95
+ // 4. Generate root index.css
96
+ if (targetFormats.includes('css')) {
97
+ generateRootIndex = await prompts.askConfirm('šŸ“„ Generate unified index.css with all selected imports?', true);
98
+ }
99
+
100
+ console.log('\nConfiguration Summary:');
101
+ console.log(` • Output: ${targetOut}`);
102
+ console.log(` • Formats: ${targetFormats.join(', ')}`);
103
+ console.log(` • Themes: ${targetThemes.join(', ')}`);
104
+
105
+ const proceed = await prompts.askConfirm('Proceed with installation?', true);
106
+ if (!proceed) {
107
+ console.log('Installation aborted.');
108
+ prompts.close();
109
+ return;
110
+ }
111
+ } finally {
112
+ prompts.close();
113
+ }
114
+ } else {
115
+ // Non-interactive / CLI flag mode
116
+ if (options.theme) {
117
+ if (options.theme === 'all') {
118
+ targetThemes = ALL_THEMES;
119
+ } else {
120
+ targetThemes = options.theme.split(',').map(s => s.trim().toLowerCase());
121
+ }
122
+ } else {
123
+ targetThemes = ALL_THEMES;
124
+ }
125
+
126
+ if (options.format) {
127
+ if (options.format === 'all') {
128
+ targetFormats = ['css', 'tailwind', 'js'];
129
+ } else {
130
+ targetFormats = options.format.split(',').map(s => s.trim().toLowerCase());
131
+ }
132
+ } else {
133
+ targetFormats = ['css'];
134
+ }
135
+ }
136
+
137
+ // Normalize JS/TS
138
+ if (targetFormats.includes('js') || targetFormats.includes('ts')) {
139
+ if (!targetFormats.includes('js')) targetFormats.push('js');
140
+ if (!targetFormats.includes('ts')) targetFormats.push('ts');
141
+ }
142
+
143
+ console.log(`\nšŸš€ Installing tokens to: \x1b[36m${path.resolve(process.cwd(), targetOut)}\x1b[0m\n`);
144
+
145
+ const destRoot = path.resolve(process.cwd(), targetOut);
146
+ if (!isDryRun && !fs.existsSync(destRoot)) {
147
+ fs.mkdirSync(destRoot, { recursive: true });
148
+ }
149
+
150
+ const installedFiles = [];
151
+
152
+ // ==========================================================================
153
+ // 1. Install CSS Variables & Selected Themes
154
+ // ==========================================================================
155
+ if (targetFormats.includes('css')) {
156
+ const cssDestDir = path.join(destRoot, 'css');
157
+ const themesDestDir = path.join(cssDestDir, 'themes');
158
+
159
+ if (!isDryRun) {
160
+ fs.mkdirSync(themesDestDir, { recursive: true });
161
+ }
162
+
163
+ // A. Base variables.css (includes Tier 1 primitives, Tier 2 semantic roles, and Default Dark theme)
164
+ const varContent = await getAssetContent('css/variables.css');
165
+ const varDestFile = path.join(cssDestDir, 'variables.css');
166
+ if (!isDryRun) {
167
+ fs.writeFileSync(varDestFile, varContent, 'utf8');
168
+ }
169
+ installedFiles.push({ path: varDestFile, label: 'Base CSS Custom Properties' });
170
+
171
+ // B. Selected Themes
172
+ const themeImports = [];
173
+ themeImports.push(`@import './variables.css';`);
174
+
175
+ for (const t of targetThemes) {
176
+ if (t === 'default') {
177
+ // default theme is baked directly into variables.css :root
178
+ continue;
179
+ }
180
+ const themeFileName = `${t}.css`;
181
+ try {
182
+ const themeContent = await getAssetContent(`css/themes/${themeFileName}`);
183
+ const themeDestFile = path.join(themesDestDir, themeFileName);
184
+ if (!isDryRun) {
185
+ fs.writeFileSync(themeDestFile, themeContent, 'utf8');
186
+ }
187
+ installedFiles.push({ path: themeDestFile, label: `Theme override: ${t}` });
188
+ themeImports.push(`@import './themes/${themeFileName}';`);
189
+ } catch (e) {
190
+ console.warn(` āš ļø Could not find theme file for: ${t}`);
191
+ }
192
+ }
193
+
194
+ // C. Unified index.css
195
+ if (generateRootIndex) {
196
+ const indexContent = `/**\n * Lemmo Design Tokens — Selected Theme Entry\n * Auto-generated by @lemmo-lab/tokens-cli\n */\n\n` +
197
+ themeImports.join('\n') + '\n';
198
+ const indexDestFile = path.join(cssDestDir, 'index.css');
199
+ if (!isDryRun) {
200
+ fs.writeFileSync(indexDestFile, indexContent, 'utf8');
201
+ }
202
+ installedFiles.push({ path: indexDestFile, label: 'Master CSS Entry Point' });
203
+ }
204
+ }
205
+
206
+ // ==========================================================================
207
+ // 2. Install Tailwind Preset
208
+ // ==========================================================================
209
+ if (targetFormats.includes('tailwind')) {
210
+ const tailwindDestDir = path.join(destRoot, 'tailwind');
211
+ if (!isDryRun) {
212
+ fs.mkdirSync(tailwindDestDir, { recursive: true });
213
+ }
214
+ const twContent = await getAssetContent('tailwind/preset.js');
215
+ const twDestFile = path.join(tailwindDestDir, 'preset.js');
216
+ if (!isDryRun) {
217
+ fs.writeFileSync(twDestFile, twContent, 'utf8');
218
+ }
219
+ installedFiles.push({ path: twDestFile, label: 'Tailwind CSS Preset' });
220
+ }
221
+
222
+ // ==========================================================================
223
+ // 3. Install JavaScript / TypeScript Tokens
224
+ // ==========================================================================
225
+ if (targetFormats.includes('js') || targetFormats.includes('ts')) {
226
+ const jsDestDir = path.join(destRoot, 'js');
227
+ if (!isDryRun) {
228
+ fs.mkdirSync(jsDestDir, { recursive: true });
229
+ }
230
+
231
+ const jsContent = await getAssetContent('js/tokens.js');
232
+ const jsDestFile = path.join(jsDestDir, 'tokens.js');
233
+ if (!isDryRun) {
234
+ fs.writeFileSync(jsDestFile, jsContent, 'utf8');
235
+ }
236
+ installedFiles.push({ path: jsDestFile, label: 'JavaScript ESM Tokens' });
237
+
238
+ const dtsContent = await getAssetContent('js/tokens.d.ts');
239
+ const dtsDestFile = path.join(jsDestDir, 'tokens.d.ts');
240
+ if (!isDryRun) {
241
+ fs.writeFileSync(dtsDestFile, dtsContent, 'utf8');
242
+ }
243
+ installedFiles.push({ path: dtsDestFile, label: 'TypeScript Token Definitions' });
244
+ }
245
+
246
+ // Summary
247
+ console.log('=============================================================');
248
+ console.log(`šŸŽ‰ \x1b[32mInstallation complete!\x1b[0m ${installedFiles.length} file(s) installed:\n`);
249
+ for (const f of installedFiles) {
250
+ const rel = path.relative(process.cwd(), f.path);
251
+ console.log(` āœ“ \x1b[36m${rel}\x1b[0m \x1b[90m(${f.label})\x1b[0m`);
252
+ }
253
+
254
+ console.log('\nšŸ’” \x1b[1mQuick Start Integration Guide:\x1b[0m');
255
+ if (targetFormats.includes('css')) {
256
+ console.log(` In your global CSS file:`);
257
+ console.log(` \x1b[33m@import './${path.posix.join(targetOut, 'css/index.css')}';\x1b[0m\n`);
258
+ console.log(` To activate a theme on a container or page:`);
259
+ for (const t of targetThemes) {
260
+ if (t === 'default') {
261
+ console.log(` <html data-theme="default"> <!-- Dark Mode (Default) -->`);
262
+ } else {
263
+ console.log(` <html data-theme="${t}"> <!-- ${t.toUpperCase()} Mode -->`);
264
+ }
265
+ }
266
+ }
267
+
268
+ if (targetFormats.includes('tailwind')) {
269
+ console.log(`\n In your tailwind.config.js:`);
270
+ console.log(` module.exports = {`);
271
+ console.log(` presets: [require('./${path.posix.join(targetOut, 'tailwind/preset.js')}')],`);
272
+ console.log(` // ...`);
273
+ console.log(` };`);
274
+ }
275
+ }
@@ -0,0 +1,31 @@
1
+ import { printBanner } from '../utils/banner.js';
2
+
3
+ export function commandList() {
4
+ printBanner();
5
+ console.log(`Available Themes & Formats in @lemmo-lab/tokens v2.0.0:\n`);
6
+
7
+ console.log(`šŸŽØ \x1b[1m\x1b[36mShipped Themes (Tier 3 Presets):\x1b[0m`);
8
+ console.log(` 1. \x1b[33mdefault\x1b[0m - Official Dark Mode (#131517 canvas, dark elevated surfaces, WCAG AA contrast)`);
9
+ console.log(` 2. \x1b[33mlight\x1b[0m - Day / Light Mode (#f8f9fa canvas, #ffffff cards, limeContrast #386b00)`);
10
+ console.log(` 3. \x1b[33mneon\x1b[0m - High-energy Cyberpunk preset (electric green #00ff66, cyan #00f0ff glow)`);
11
+ console.log(` 4. \x1b[33mmidnight\x1b[0m - OLED Pure Black minimal theme (#000000 canvas, ultra-high contrast)\n`);
12
+
13
+ console.log(`šŸ“¦ \x1b[1m\x1b[36mSupported Output Formats:\x1b[0m`);
14
+ console.log(` • \x1b[32mcss\x1b[0m - Standard CSS Custom Properties (--lemmo-*) with responsive themes`);
15
+ console.log(` • \x1b[32mtailwind\x1b[0m - Tailwind CSS v3 & v4 compatible preset (preset.js)`);
16
+ console.log(` • \x1b[32mjs / ts\x1b[0m - Type-safe JavaScript / TypeScript token dictionary\n`);
17
+
18
+ console.log(`šŸ“ \x1b[1m\x1b[36mToken Scales Included:\x1b[0m`);
19
+ console.log(` • Colors: Brand, Surface, Text, Border, Status, Alpha overlays`);
20
+ console.log(` • Gradients: All 26 fully-tokenized gradients`);
21
+ console.log(` • Typography: Satoshi, IRANSans, Morabba, Oddval with unitless leading ratios`);
22
+ console.log(` • Spacing: 4px rhythmic scale (0 to 2400)`);
23
+ console.log(` • Radius: Relative scale computed from base (--radius)`);
24
+ console.log(` • Elevation: Ambient & drop shadows, inner rims, glass reflections`);
25
+ console.log(` • Motion: Fast, Normal, Slow durations and cubic bezier curves\n`);
26
+
27
+ console.log(`Usage:`);
28
+ console.log(` \x1b[1mtokens install\x1b[0m # Interactive setup`);
29
+ console.log(` \x1b[1mtokens install --theme light\x1b[0m # Install only light theme`);
30
+ console.log(` \x1b[1mtokens install --theme neon,midnight\x1b[0m # Install specific themes`);
31
+ }
@@ -0,0 +1,8 @@
1
+ export function printBanner() {
2
+ console.log(`
3
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
4
+ │ ⚔ Lemmo Design Tokens CLI Tool (v2.0.0) │
5
+ │ Enterprise asset generator, theme installer & synchronizer │
6
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
7
+ `);
8
+ }
@@ -0,0 +1,67 @@
1
+ import readline from 'node:readline/promises';
2
+ import { stdin as input, stdout as output } from 'node:process';
3
+
4
+ export async function createPromptSession() {
5
+ const rl = readline.createInterface({ input, output });
6
+
7
+ return {
8
+ async askText(question, defaultValue = '') {
9
+ const promptStr = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
10
+ const answer = await rl.question(promptStr);
11
+ return answer.trim() || defaultValue;
12
+ },
13
+
14
+ async askConfirm(question, defaultYes = true) {
15
+ const hint = defaultYes ? '(Y/n)' : '(y/N)';
16
+ const answer = await rl.question(`${question} ${hint} `);
17
+ const cleaned = answer.trim().toLowerCase();
18
+ if (!cleaned) return defaultYes;
19
+ return cleaned === 'y' || cleaned === 'yes';
20
+ },
21
+
22
+ async askChoice(question, options, defaultIndex = 0) {
23
+ console.log(`\n${question}`);
24
+ options.forEach((opt, idx) => {
25
+ const marker = idx === defaultIndex ? 'āž”' : ' ';
26
+ console.log(` ${marker} ${idx + 1}) ${opt.label || opt}`);
27
+ });
28
+ const answer = await rl.question(`Select [1-${options.length}] (default: ${defaultIndex + 1}): `);
29
+ const num = parseInt(answer.trim(), 10);
30
+ if (isNaN(num) || num < 1 || num > options.length) {
31
+ return options[defaultIndex].value !== undefined ? options[defaultIndex].value : options[defaultIndex];
32
+ }
33
+ const sel = options[num - 1];
34
+ return sel.value !== undefined ? sel.value : sel;
35
+ },
36
+
37
+ async askMultiChoice(question, options, defaultSelectedIndices = []) {
38
+ console.log(`\n${question} (enter comma-separated numbers, e.g. 1,2 or hit enter for default):`);
39
+ options.forEach((opt, idx) => {
40
+ const isDef = defaultSelectedIndices.includes(idx);
41
+ const marker = isDef ? 'āœ“' : ' ';
42
+ console.log(` [${marker}] ${idx + 1}) ${opt.label || opt}`);
43
+ });
44
+
45
+ const answer = await rl.question(`Select numbers (default: ${defaultSelectedIndices.map(i => i + 1).join(',')}): `);
46
+ const cleaned = answer.trim();
47
+ if (!cleaned) {
48
+ return defaultSelectedIndices.map(i => options[i].value !== undefined ? options[i].value : options[i]);
49
+ }
50
+
51
+ const indices = cleaned
52
+ .split(',')
53
+ .map(s => parseInt(s.trim(), 10) - 1)
54
+ .filter(i => !isNaN(i) && i >= 0 && i < options.length);
55
+
56
+ if (indices.length === 0) {
57
+ return defaultSelectedIndices.map(i => options[i].value !== undefined ? options[i].value : options[i]);
58
+ }
59
+
60
+ return indices.map(i => options[i].value !== undefined ? options[i].value : options[i]);
61
+ },
62
+
63
+ close() {
64
+ rl.close();
65
+ }
66
+ };
67
+ }
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@lemmo-lab/tokens",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Single source of truth design tokens for Lemmo products",
5
5
  "type": "module",
6
6
  "main": "./dist/js/tokens.js",
7
7
  "module": "./dist/js/tokens.js",
8
8
  "types": "./dist/js/tokens.d.ts",
9
+ "bin": {
10
+ "lemmo-tokens": "./dist/cli/bin/cli.js",
11
+ "tokens": "./dist/cli/bin/cli.js"
12
+ },
9
13
  "exports": {
10
14
  ".": {
11
15
  "types": "./dist/js/tokens.d.ts",