@stacklist-app/brandkit 1.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.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # @stacklist-app/brandkit
2
+
3
+ A config-driven brand guide that bolts onto any website. One `config.json` drives the entire guide — colors, typography, logos, voice, components, spacing, accessibility. Zero runtime dependencies.
4
+
5
+ Swap the config and assets to generate a brand guide for any project. An optional `generate` command scrapes an existing codebase (Tailwind config, CSS variables, logo files) to bootstrap the config automatically.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install github:The-Stack-Lab/brandkit
11
+ # or pin to a tag
12
+ npm install github:The-Stack-Lab/brandkit#v1.1.1
13
+ ```
14
+
15
+ Requires Node.js 18+.
16
+
17
+ ## Quickstart
18
+
19
+ ```bash
20
+ npx brandkit init brand # scaffold brand/config.json + starter assets
21
+ npx brandkit generate brand # optional: auto-extract tokens from your codebase
22
+ npx brandkit dev brand # preview at http://localhost:4800 (live reload)
23
+ npx brandkit build brand # bake into static files for production
24
+ ```
25
+
26
+ The `brand` directory name is your choice — pass any path.
27
+
28
+ ## CLI
29
+
30
+ | Command | Description |
31
+ |---|---|
32
+ | `brandkit init [dir]` | Scaffold a new brand guide with a starter `config.json` |
33
+ | `brandkit generate [dir]` | Extract colors, fonts, spacing, and logos from the host codebase and merge into `config.json`. Manual fields (voice, accessibility, components) are preserved. |
34
+ | `brandkit dev [dir]` | Local dev server on `:4800` with SSE live reload |
35
+ | `brandkit build [dir]` | Produce static `index.html` + `styles.css` + `engine.js` with the theme baked in |
36
+
37
+ ## Framework integrations
38
+
39
+ Serve the brand guide at `/brand` in your existing dev server and bundle it on build.
40
+
41
+ **Vite**
42
+ ```js
43
+ // vite.config.js
44
+ import brandkit from '@stacklist-app/brandkit/integrations/vite'
45
+
46
+ export default {
47
+ plugins: [brandkit()],
48
+ }
49
+ ```
50
+
51
+ **Astro**
52
+ ```js
53
+ // astro.config.mjs
54
+ import brandkit from '@stacklist-app/brandkit/integrations/astro'
55
+
56
+ export default {
57
+ integrations: [brandkit()],
58
+ }
59
+ ```
60
+
61
+ **Next.js / plain HTML**: run `brandkit build brand` and serve the resulting directory as a static route.
62
+
63
+ ## Config
64
+
65
+ `config.json` is the single source of truth. Top-level keys:
66
+
67
+ - `brand` — name, tagline, description, version
68
+ - `fonts` — display + body with Google Fonts import
69
+ - `theme` — CSS variable map (colors, gradients, font vars)
70
+ - `nav` — sidebar structure
71
+ - `colors` — brand / neutrals / semantic palettes
72
+ - `gradients`, `gradientUsage` — gradient definitions + do/don't lists
73
+ - `logos`, `logoSizes` — logo variants + download sizes
74
+ - `typography` — type scale and specimens
75
+ - `hierarchy` — text hierarchy demo
76
+ - `voice` — tone description + do/don't examples
77
+ - `components` — buttons, cards, stats
78
+ - `spacing` — spacing scale tokens
79
+ - `accessibility` — contrast ratio grid
80
+ - `cssVariables` — variable reference
81
+
82
+ See `example/config.json` for a complete reference implementation (Freeway PHX).
83
+
84
+ ## How theming works
85
+
86
+ `dist/styles.css` uses CSS custom properties (`var(--purple)`, `var(--font-display)`, etc.) with no `:root` block.
87
+
88
+ - **Dev**: the engine reads `config.theme` and injects a `<style data-brandkit-theme>` tag at runtime.
89
+ - **Build**: `brandkit build` prepends the generated `:root` block to `styles.css` and injects the Google Fonts `<link>` and page title into `index.html`.
90
+
91
+ Swap the config, and every color, gradient, and font token updates everywhere.
92
+
93
+ ## License
94
+
95
+ MIT © The Stack Lab
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ var args = process.argv.slice(2);
4
+ var command = args[0] || 'help';
5
+
6
+ switch (command) {
7
+ case 'init':
8
+ require('../cli/init')(args.slice(1));
9
+ break;
10
+ case 'dev':
11
+ require('../cli/dev')(args.slice(1));
12
+ break;
13
+ case 'build':
14
+ require('../cli/build')(args.slice(1));
15
+ break;
16
+ case 'generate':
17
+ require('../cli/generate')(args.slice(1));
18
+ break;
19
+ case 'help':
20
+ case '--help':
21
+ case '-h':
22
+ printHelp();
23
+ break;
24
+ case '--version':
25
+ case '-v':
26
+ var pkg = require('../package.json');
27
+ console.log(pkg.version);
28
+ break;
29
+ default:
30
+ console.error('Unknown command: ' + command);
31
+ printHelp();
32
+ process.exit(1);
33
+ }
34
+
35
+ function printHelp() {
36
+ console.log('');
37
+ console.log(' brandkit — config-driven brand guide');
38
+ console.log('');
39
+ console.log(' Usage:');
40
+ console.log(' brandkit init [dir] Scaffold a new brand guide');
41
+ console.log(' brandkit generate [dir] Auto-generate config from codebase');
42
+ console.log(' brandkit dev [dir] Start dev server with live reload');
43
+ console.log(' brandkit build [dir] Build static files for production');
44
+ console.log('');
45
+ console.log(' Options:');
46
+ console.log(' --help, -h Show this help message');
47
+ console.log(' --version, -v Show version number');
48
+ console.log('');
49
+ }
package/cli/build.js ADDED
@@ -0,0 +1,35 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+ var resolve = require('../lib/resolve');
4
+ var template = require('../lib/template');
5
+
6
+ module.exports = function build(args) {
7
+ var targetDir = path.resolve(args[0] || '.');
8
+ var distDir = resolve.getDistPath();
9
+
10
+ var configPath = path.join(targetDir, 'config.json');
11
+ if (!fs.existsSync(configPath)) {
12
+ console.error('');
13
+ console.error(' No config.json found in ' + targetDir);
14
+ console.error(' Run: brandkit init ' + path.relative(process.cwd(), targetDir));
15
+ console.error('');
16
+ process.exit(1);
17
+ }
18
+
19
+ console.log('');
20
+ console.log(' brandkit build');
21
+ console.log('');
22
+ console.log(' Reading ' + path.relative(process.cwd(), configPath) + '...');
23
+
24
+ var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
25
+
26
+ template.build(distDir, targetDir, config);
27
+
28
+ var brandName = (config.brand && config.brand.displayName) || 'Brand';
29
+ console.log(' built index.html (' + brandName + ' title + fonts baked in)');
30
+ console.log(' built styles.css (:root variables generated)');
31
+ console.log(' copied engine.js');
32
+ console.log('');
33
+ console.log(' Ready to deploy. Serve ' + path.relative(process.cwd(), targetDir) + '/ as static files.');
34
+ console.log('');
35
+ };
package/cli/dev.js ADDED
@@ -0,0 +1,129 @@
1
+ var http = require('http');
2
+ var fs = require('fs');
3
+ var path = require('path');
4
+ var url = require('url');
5
+
6
+ var MIME_TYPES = {
7
+ '.html': 'text/html',
8
+ '.css': 'text/css',
9
+ '.js': 'application/javascript',
10
+ '.json': 'application/json',
11
+ '.svg': 'image/svg+xml',
12
+ '.png': 'image/png',
13
+ '.jpg': 'image/jpeg',
14
+ '.jpeg': 'image/jpeg',
15
+ '.ico': 'image/x-icon',
16
+ '.woff': 'font/woff',
17
+ '.woff2': 'font/woff2'
18
+ };
19
+
20
+ module.exports = function dev(args) {
21
+ var dir = path.resolve(args[0] || '.');
22
+ var port = 4800;
23
+
24
+ // Parse --port flag
25
+ for (var i = 0; i < args.length; i++) {
26
+ if (args[i] === '--port' && args[i + 1]) {
27
+ port = parseInt(args[i + 1], 10) || 4800;
28
+ }
29
+ }
30
+
31
+ // Validate directory has a config.json
32
+ var configPath = path.join(dir, 'config.json');
33
+ if (!fs.existsSync(configPath)) {
34
+ console.error('');
35
+ console.error(' No config.json found in ' + dir);
36
+ console.error(' Run: brandkit init ' + path.relative(process.cwd(), dir));
37
+ console.error('');
38
+ process.exit(1);
39
+ }
40
+
41
+ // SSE clients for live reload
42
+ var sseClients = [];
43
+
44
+ var server = http.createServer(function (req, res) {
45
+ var parsed = url.parse(req.url);
46
+ var pathname = parsed.pathname;
47
+
48
+ // SSE endpoint for live reload
49
+ if (pathname === '/__brandkit_reload') {
50
+ res.writeHead(200, {
51
+ 'Content-Type': 'text/event-stream',
52
+ 'Cache-Control': 'no-cache',
53
+ 'Connection': 'keep-alive',
54
+ 'Access-Control-Allow-Origin': '*'
55
+ });
56
+ res.write('data: connected\n\n');
57
+ sseClients.push(res);
58
+ req.on('close', function () {
59
+ var idx = sseClients.indexOf(res);
60
+ if (idx !== -1) sseClients.splice(idx, 1);
61
+ });
62
+ return;
63
+ }
64
+
65
+ // Serve static files
66
+ if (pathname === '/') pathname = '/index.html';
67
+ var filePath = path.join(dir, pathname);
68
+
69
+ // Security: prevent path traversal
70
+ if (!filePath.startsWith(dir)) {
71
+ res.writeHead(403);
72
+ res.end('Forbidden');
73
+ return;
74
+ }
75
+
76
+ fs.readFile(filePath, function (err, data) {
77
+ if (err) {
78
+ res.writeHead(404);
79
+ res.end('Not found: ' + pathname);
80
+ return;
81
+ }
82
+ var ext = path.extname(filePath).toLowerCase();
83
+ var mime = MIME_TYPES[ext] || 'application/octet-stream';
84
+ res.writeHead(200, { 'Content-Type': mime });
85
+
86
+ // Inject live reload script into HTML
87
+ if (ext === '.html') {
88
+ var html = data.toString();
89
+ var reloadScript =
90
+ '<script>' +
91
+ '(function(){' +
92
+ 'var es=new EventSource("/__brandkit_reload");' +
93
+ 'es.onmessage=function(e){if(e.data==="reload")location.reload();};' +
94
+ 'es.onerror=function(){es.close();setTimeout(function(){location.reload();},2000);};' +
95
+ '})();' +
96
+ '</script>';
97
+ html = html.replace('</body>', reloadScript + '</body>');
98
+ res.end(html);
99
+ } else {
100
+ res.end(data);
101
+ }
102
+ });
103
+ });
104
+
105
+ server.listen(port, function () {
106
+ console.log('');
107
+ console.log(' brandkit dev');
108
+ console.log('');
109
+ console.log(' Serving brand guide at http://localhost:' + port + '/');
110
+ console.log(' Watching ' + path.relative(process.cwd(), configPath) + ' for changes...');
111
+ console.log('');
112
+ });
113
+
114
+ // Watch config.json for changes
115
+ var debounce = null;
116
+ fs.watch(dir, { recursive: true }, function (eventType, filename) {
117
+ if (!filename) return;
118
+ // Only reload on relevant file changes
119
+ var ext = path.extname(filename).toLowerCase();
120
+ if (['.json', '.html', '.css', '.js'].indexOf(ext) === -1) return;
121
+
122
+ clearTimeout(debounce);
123
+ debounce = setTimeout(function () {
124
+ sseClients.forEach(function (client) {
125
+ client.write('data: reload\n\n');
126
+ });
127
+ }, 100);
128
+ });
129
+ };
@@ -0,0 +1,272 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+ var extractTailwind = require('../lib/extract-tailwind');
4
+ var extractCSS = require('../lib/extract-css');
5
+ var extractLogos = require('../lib/extract-logos');
6
+ var helpers = require('../lib/generate-helpers');
7
+ var schema = require('../lib/config-schema');
8
+
9
+ module.exports = function generate(args) {
10
+ var brandDir = path.resolve(args[0] || 'brand');
11
+ var projectDir = process.cwd();
12
+
13
+ console.log('');
14
+ console.log(' brandkit generate');
15
+ console.log('');
16
+ console.log(' Scanning project...');
17
+
18
+ var extracted = {};
19
+ var summary = [];
20
+
21
+ // Extract from Tailwind config
22
+ var tw = extractTailwind.extract(projectDir);
23
+ if (tw.colors) {
24
+ summary.push(' Found Tailwind config \u2014 extracting colors, fonts, spacing');
25
+ extracted.tailwindColors = tw.colors;
26
+ }
27
+ if (tw.fonts) extracted.tailwindFonts = tw.fonts;
28
+ if (tw.spacing) extracted.tailwindSpacing = tw.spacing;
29
+
30
+ // Extract CSS custom properties
31
+ var cssVars = extractCSS.extract(projectDir);
32
+ if (cssVars) {
33
+ summary.push(' Found CSS custom properties \u2014 extracting theme variables');
34
+ extracted.cssVars = cssVars;
35
+ }
36
+
37
+ // Extract logos
38
+ var logos = extractLogos.extract(projectDir);
39
+ if (logos && logos.length) {
40
+ var logoNames = logos.map(function (l) { return path.basename(Object.values(l.variants)[0]); });
41
+ summary.push(' Found ' + logoNames.join(', ') + ' \u2014 adding to logos');
42
+ extracted.logos = logos;
43
+ }
44
+
45
+ if (!summary.length) {
46
+ summary.push(' No Tailwind config, CSS variables, or logo assets detected');
47
+ summary.push(' Creating starter config with example entries');
48
+ }
49
+
50
+ // Load existing config or create starter
51
+ var configPath = path.join(brandDir, 'config.json');
52
+ var existingConfig = null;
53
+ if (fs.existsSync(configPath)) {
54
+ try {
55
+ existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
56
+ console.log(' Found existing config.json \u2014 preserving manual fields');
57
+ } catch (_) {
58
+ console.log(' Existing config.json is invalid \u2014 creating fresh');
59
+ }
60
+ }
61
+
62
+ var baseConfig = existingConfig || schema.starterConfig();
63
+
64
+ // Build extracted config fields
65
+ var newFields = {};
66
+
67
+ // Colors from Tailwind (with auto-computed oklch and light flag)
68
+ if (extracted.tailwindColors) {
69
+ newFields.colors = buildColors(extracted.tailwindColors);
70
+ }
71
+
72
+ // Theme from CSS variables
73
+ if (extracted.cssVars) {
74
+ newFields.theme = extractCSS.mapToTheme(extracted.cssVars);
75
+ }
76
+
77
+ // Fonts from Tailwind
78
+ if (extracted.tailwindFonts) {
79
+ var fonts = {};
80
+ if (extracted.tailwindFonts.display) {
81
+ fonts.display = {
82
+ family: extracted.tailwindFonts.display.family,
83
+ googleImport: extracted.tailwindFonts.display.family + ':wght@300;400;500;600;700',
84
+ description: '__TODO: Describe the display font.'
85
+ };
86
+ }
87
+ if (extracted.tailwindFonts.body) {
88
+ fonts.body = {
89
+ family: extracted.tailwindFonts.body.family,
90
+ googleImport: extracted.tailwindFonts.body.family + ':wght@300;400;500;600;700',
91
+ description: '__TODO: Describe the body font.'
92
+ };
93
+ }
94
+ if (Object.keys(fonts).length) newFields.fonts = fonts;
95
+ }
96
+
97
+ // Spacing from Tailwind
98
+ if (extracted.tailwindSpacing) {
99
+ newFields.spacing = extracted.tailwindSpacing;
100
+ }
101
+
102
+ // Logos (make paths relative to brand dir)
103
+ if (extracted.logos) {
104
+ newFields.logos = extracted.logos.map(function (logo) {
105
+ var variants = {};
106
+ var keys = Object.keys(logo.variants);
107
+ for (var i = 0; i < keys.length; i++) {
108
+ variants[keys[i]] = path.relative(brandDir, path.join(projectDir, logo.variants[keys[i]]));
109
+ }
110
+ return {
111
+ name: logo.name,
112
+ description: logo.description,
113
+ variants: variants,
114
+ background: logo.background
115
+ };
116
+ });
117
+ }
118
+
119
+ // --- Auto-compute derived fields ---
120
+ // Only overwrite if existing field is empty or all-scaffold (__TODO)
121
+
122
+ // Determine the theme to derive from (extracted or existing)
123
+ var theme = newFields.theme || baseConfig.theme;
124
+
125
+ // Auto-generate gradients from theme
126
+ if (theme && isEmptyOrScaffold(baseConfig.gradients)) {
127
+ var gradients = helpers.buildGradientsFromTheme(theme);
128
+ if (gradients.length) {
129
+ newFields.gradients = gradients;
130
+ summary.push(' Gradients: ' + gradients.length + ' auto-generated from theme');
131
+ }
132
+ }
133
+
134
+ // Auto-generate hierarchy from theme
135
+ if (theme && isEmptyOrScaffold(baseConfig.hierarchy)) {
136
+ newFields.hierarchy = helpers.buildHierarchyFromTheme(theme);
137
+ summary.push(' Hierarchy: 4 levels auto-generated from theme colors');
138
+ }
139
+
140
+ // Auto-generate accessibility pairs from all extracted colors
141
+ if (isEmptyOrScaffold(baseConfig.accessibility)) {
142
+ var allColors = [];
143
+ var colorSource = newFields.colors || baseConfig.colors;
144
+ if (colorSource) {
145
+ ['brand', 'neutrals', 'semantic'].forEach(function (key) {
146
+ var group = colorSource[key];
147
+ if (group) {
148
+ var items = group.items || group;
149
+ if (Array.isArray(items)) {
150
+ allColors = allColors.concat(items);
151
+ }
152
+ }
153
+ });
154
+ }
155
+ if (allColors.length) {
156
+ var a11y = helpers.generateA11yPairs(allColors);
157
+ if (a11y.length) {
158
+ newFields.accessibility = a11y;
159
+ summary.push(' Accessibility: ' + a11y.length + ' contrast pairs auto-computed');
160
+ }
161
+ }
162
+ }
163
+
164
+ // Auto-generate cssVariables from theme
165
+ if (theme && isEmptyOrScaffold(baseConfig.cssVariables)) {
166
+ var cssVarSections = helpers.buildCssVariablesFromTheme(theme);
167
+ if (cssVarSections.length) {
168
+ newFields.cssVariables = cssVarSections;
169
+ summary.push(' CSS Variables: ' + cssVarSections.length + ' sections auto-generated');
170
+ }
171
+ }
172
+
173
+ // Auto-scaffold typography if empty or missing
174
+ if (isEmptyOrScaffold(baseConfig.typography)) {
175
+ newFields.typography = helpers.scaffoldTypography();
176
+ summary.push(' Typography: standard type scale scaffolded');
177
+ }
178
+
179
+ // Print all summary messages (including derived fields computed above)
180
+ summary.forEach(function (line) { console.log(line); });
181
+
182
+ // Merge
183
+ var finalConfig = schema.mergeConfigs(baseConfig, newFields);
184
+
185
+ // Ensure brand dir exists
186
+ if (!fs.existsSync(brandDir)) {
187
+ fs.mkdirSync(brandDir, { recursive: true });
188
+ }
189
+
190
+ fs.writeFileSync(configPath, JSON.stringify(finalConfig, null, 2) + '\n');
191
+
192
+ // Count what needs TODO attention
193
+ var todoCount = countTodos(finalConfig);
194
+
195
+ console.log('');
196
+ console.log(' Generated ' + path.relative(process.cwd(), configPath));
197
+ if (extracted.tailwindColors) console.log(' Colors: ' + extracted.tailwindColors.length + ' extracted (with oklch)');
198
+ if (extracted.tailwindFonts) console.log(' Fonts: ' + Object.keys(extracted.tailwindFonts).length + ' detected');
199
+ if (extracted.tailwindSpacing) console.log(' Spacing: ' + extracted.tailwindSpacing.length + ' tokens');
200
+ if (extracted.logos) console.log(' Logos: ' + extracted.logos.length + ' files found');
201
+ if (todoCount > 0) console.log(' TODO: ' + todoCount + ' fields need manual or AI attention');
202
+ console.log('');
203
+ };
204
+
205
+ function buildColors(colorList) {
206
+ var brand = [];
207
+ var neutrals = [];
208
+ var semantic = [];
209
+
210
+ var semanticNames = ['success', 'warning', 'error', 'danger', 'info'];
211
+ var neutralNames = ['gray', 'grey', 'slate', 'zinc', 'neutral', 'stone', 'black', 'white'];
212
+
213
+ for (var i = 0; i < colorList.length; i++) {
214
+ var c = colorList[i];
215
+ // Skip colors without valid hex (Tailwind function-based colors, etc.)
216
+ if (!c.hex || typeof c.hex !== 'string' || !/^#[0-9a-fA-F]{3,8}$/.test(c.hex)) continue;
217
+ if (!c.name) continue;
218
+ var lowerName = c.name.toLowerCase();
219
+
220
+ var isSemantic = false;
221
+ var isNeutral = false;
222
+ for (var j = 0; j < semanticNames.length; j++) {
223
+ if (lowerName.indexOf(semanticNames[j]) !== -1) { isSemantic = true; break; }
224
+ }
225
+ if (!isSemantic) {
226
+ for (var k = 0; k < neutralNames.length; k++) {
227
+ if (lowerName.indexOf(neutralNames[k]) !== -1) { isNeutral = true; break; }
228
+ }
229
+ }
230
+
231
+ // Auto-compute oklch and light flag
232
+ var entry = {
233
+ name: c.name,
234
+ hex: c.hex,
235
+ oklch: helpers.hexToOklch(c.hex),
236
+ cssVar: '--color-' + c.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
237
+ role: c.role || '',
238
+ light: helpers.isLightColor(c.hex)
239
+ };
240
+
241
+ if (isSemantic) semantic.push(entry);
242
+ else if (isNeutral) neutrals.push(entry);
243
+ else brand.push(entry);
244
+ }
245
+
246
+ return {
247
+ brand: { label: 'Brand', items: brand },
248
+ neutrals: { label: 'Neutrals', items: neutrals },
249
+ semantic: { label: 'Semantic', items: semantic }
250
+ };
251
+ }
252
+
253
+ function countTodos(obj) {
254
+ var count = 0;
255
+ var str = JSON.stringify(obj);
256
+ var regex = /__TODO/g;
257
+ while (regex.exec(str) !== null) count++;
258
+ return count;
259
+ }
260
+
261
+ /**
262
+ * Check if a field is empty or only contains scaffold/placeholder data.
263
+ * Returns true if the field should be overwritten by auto-generation.
264
+ */
265
+ function isEmptyOrScaffold(value) {
266
+ if (!value) return true;
267
+ if (Array.isArray(value) && value.length === 0) return true;
268
+ // Check if all entries are scaffold (contain __TODO)
269
+ var str = JSON.stringify(value);
270
+ if (str.indexOf('__TODO') !== -1) return true;
271
+ return false;
272
+ }
package/cli/init.js ADDED
@@ -0,0 +1,62 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+ var resolve = require('../lib/resolve');
4
+ var schema = require('../lib/config-schema');
5
+
6
+ module.exports = function init(args) {
7
+ var targetDir = path.resolve(args[0] || 'brand');
8
+ var distDir = resolve.getDistPath();
9
+
10
+ console.log('');
11
+ console.log(' brandkit');
12
+ console.log('');
13
+ console.log(' Scaffolding into ' + path.relative(process.cwd(), targetDir) + '/...');
14
+
15
+ // Create target directory
16
+ if (!fs.existsSync(targetDir)) {
17
+ fs.mkdirSync(targetDir, { recursive: true });
18
+ }
19
+
20
+ // Create logos directory
21
+ var logosDir = path.join(targetDir, 'logos');
22
+ if (!fs.existsSync(logosDir)) {
23
+ fs.mkdirSync(logosDir, { recursive: true });
24
+ }
25
+
26
+ // Check for --update flag: only update engine files, preserve config
27
+ var isUpdate = args.indexOf('--update') !== -1;
28
+
29
+ // Copy engine files from dist/
30
+ var files = ['engine.js', 'styles.css', 'index.html'];
31
+ for (var i = 0; i < files.length; i++) {
32
+ var src = path.join(distDir, files[i]);
33
+ var dest = path.join(targetDir, files[i]);
34
+ if (fs.existsSync(src)) {
35
+ fs.copyFileSync(src, dest);
36
+ console.log(' copied ' + files[i]);
37
+ }
38
+ }
39
+
40
+ // Create starter config.json (skip if exists and not forcing)
41
+ var configPath = path.join(targetDir, 'config.json');
42
+ if (!fs.existsSync(configPath) && !isUpdate) {
43
+ var starter = schema.starterConfig();
44
+ fs.writeFileSync(configPath, JSON.stringify(starter, null, 2) + '\n');
45
+ console.log(' created config.json');
46
+ } else if (isUpdate) {
47
+ console.log(' kept config.json (--update)');
48
+ } else {
49
+ console.log(' kept config.json (already exists)');
50
+ }
51
+
52
+ console.log('');
53
+ if (isUpdate) {
54
+ console.log(' Updated engine files. Config and logos preserved.');
55
+ } else {
56
+ console.log(' Next steps:');
57
+ console.log(' 1. Edit ' + path.relative(process.cwd(), configPath) + ' with your brand data');
58
+ console.log(' 2. Add logo files to ' + path.relative(process.cwd(), logosDir) + '/');
59
+ console.log(' 3. Run: npx brandkit dev ' + path.relative(process.cwd(), targetDir));
60
+ }
61
+ console.log('');
62
+ };