brandspec 0.1.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.
package/dist/cli.js ADDED
@@ -0,0 +1,1516 @@
1
+ #!/usr/bin/env node
2
+
3
+ // cli/cli.ts
4
+ import {
5
+ readFileSync as readFileSync2,
6
+ writeFileSync as writeFileSync2,
7
+ existsSync as existsSync2,
8
+ statSync,
9
+ mkdirSync as mkdirSync2,
10
+ cpSync,
11
+ readdirSync,
12
+ unlinkSync
13
+ } from "fs";
14
+ import { resolve as resolve2, dirname as dirname2, join as join2 } from "path";
15
+ import { fileURLToPath } from "url";
16
+ import { createInterface } from "readline";
17
+
18
+ // cli/parser.ts
19
+ import yaml from "js-yaml";
20
+ function parse(content) {
21
+ const errors = [];
22
+ const warnings = [];
23
+ let parsed;
24
+ try {
25
+ parsed = yaml.load(content);
26
+ } catch (e) {
27
+ return {
28
+ success: false,
29
+ errors: [`Invalid YAML: ${e instanceof Error ? e.message : "Unknown error"}`],
30
+ warnings: []
31
+ };
32
+ }
33
+ if (!parsed || typeof parsed !== "object") {
34
+ return { success: false, errors: ["YAML must be an object"], warnings: [] };
35
+ }
36
+ const doc = parsed;
37
+ if (!doc.meta || typeof doc.meta !== "object") {
38
+ errors.push("Missing required field: meta");
39
+ return { success: false, errors, warnings };
40
+ }
41
+ const meta = doc.meta;
42
+ if (!meta.name || typeof meta.name !== "string") {
43
+ errors.push("Missing required field: meta.name");
44
+ return { success: false, errors, warnings };
45
+ }
46
+ if (doc.assets !== void 0) {
47
+ if (!Array.isArray(doc.assets)) {
48
+ errors.push("Field 'assets' must be an array");
49
+ } else {
50
+ doc.assets.forEach((asset, i) => {
51
+ if (!asset || typeof asset !== "object") {
52
+ errors.push(`assets[${i}] must be an object`);
53
+ } else {
54
+ const a = asset;
55
+ if (!a.file || typeof a.file !== "string") {
56
+ errors.push(`assets[${i}].file is required and must be a string`);
57
+ }
58
+ }
59
+ });
60
+ }
61
+ }
62
+ if (!doc.core) {
63
+ warnings.push("No 'core' section \u2014 brand essence, personality, and voice are recommended");
64
+ }
65
+ if (!doc.tokens) {
66
+ warnings.push("No 'tokens' section \u2014 design tokens are recommended");
67
+ }
68
+ if (errors.length > 0) {
69
+ return { success: false, errors, warnings };
70
+ }
71
+ return {
72
+ success: true,
73
+ data: doc,
74
+ errors: [],
75
+ warnings
76
+ };
77
+ }
78
+
79
+ // cli/validate.ts
80
+ import Ajv from "ajv";
81
+ import addFormats from "ajv-formats";
82
+
83
+ // cli/schema.ts
84
+ var schema = {
85
+ $schema: "https://json-schema.org/draft/2020-12/schema",
86
+ $id: "https://brandspec.tools/schema/v0.1.0",
87
+ title: "brandspec",
88
+ description: "Brand Identity specification format",
89
+ type: "object",
90
+ required: ["meta"],
91
+ properties: {
92
+ meta: {
93
+ type: "object",
94
+ description: "Brand metadata",
95
+ required: ["name"],
96
+ properties: {
97
+ name: { type: "string", description: "Brand name" },
98
+ version: { type: "string", description: "Brand spec version (semver)" },
99
+ updated: { type: "string", format: "date", description: "Last updated date" },
100
+ description: { type: "string", description: "Brief brand description" },
101
+ url: { type: "string", format: "uri", description: "Brand website" }
102
+ },
103
+ additionalProperties: true
104
+ },
105
+ core: {
106
+ type: "object",
107
+ description: "Brand essence, personality, and voice",
108
+ properties: {
109
+ essence: { type: "string" },
110
+ tagline: { type: "string" },
111
+ mission: { type: "string" },
112
+ vision: { type: "string" },
113
+ values: { type: "array", items: { type: "string" } },
114
+ personality: { type: "array", items: { type: "string" } },
115
+ voice: {
116
+ type: "object",
117
+ properties: {
118
+ tone: { type: "array", items: { type: "string" } },
119
+ principles: { type: "array", items: { type: "string" } }
120
+ },
121
+ additionalProperties: true
122
+ }
123
+ },
124
+ additionalProperties: true
125
+ },
126
+ tokens: {
127
+ type: "object",
128
+ description: "Design tokens (W3C DTCG compliant)",
129
+ additionalProperties: true
130
+ },
131
+ assets: {
132
+ type: "array",
133
+ description: "Brand assets",
134
+ items: {
135
+ type: "object",
136
+ required: ["file"],
137
+ properties: {
138
+ file: { type: "string" },
139
+ id: { type: "string" },
140
+ role: { type: "string" },
141
+ variant: { type: "string" },
142
+ context: { type: "string" },
143
+ description: { type: "string" },
144
+ formats: {
145
+ type: "array",
146
+ items: {
147
+ type: "object",
148
+ properties: {
149
+ path: { type: "string" },
150
+ width: { type: "integer" },
151
+ height: { type: "integer" }
152
+ },
153
+ additionalProperties: true
154
+ }
155
+ },
156
+ tags: { type: "array", items: { type: "string" } }
157
+ },
158
+ additionalProperties: true
159
+ }
160
+ },
161
+ guidelines: {
162
+ type: "object",
163
+ description: "Usage guidelines",
164
+ additionalProperties: {
165
+ type: "object",
166
+ properties: {
167
+ content: { type: "string" },
168
+ rules: {
169
+ type: "array",
170
+ items: { $ref: "#/$defs/guidelineRule" }
171
+ }
172
+ },
173
+ additionalProperties: true
174
+ }
175
+ },
176
+ extensions: {
177
+ type: "object",
178
+ description: "Custom extensions",
179
+ additionalProperties: true
180
+ }
181
+ },
182
+ additionalProperties: true,
183
+ $defs: {
184
+ guidelineRule: {
185
+ type: "object",
186
+ required: ["description", "severity"],
187
+ properties: {
188
+ id: { type: "string" },
189
+ description: { type: "string" },
190
+ severity: {
191
+ type: "string",
192
+ enum: ["info", "warning", "error"]
193
+ },
194
+ criteria: { type: "array", items: { type: "string" } },
195
+ applies_to: { type: "string" }
196
+ },
197
+ additionalProperties: true
198
+ }
199
+ }
200
+ };
201
+
202
+ // cli/validate.ts
203
+ var ajvInstance = null;
204
+ function getAjv() {
205
+ if (!ajvInstance) {
206
+ ajvInstance = new Ajv({ allErrors: true, strict: false });
207
+ addFormats(ajvInstance);
208
+ }
209
+ return ajvInstance;
210
+ }
211
+ function validate(data) {
212
+ const ajv = getAjv();
213
+ const { $schema: _, $id: __, ...schemaBody } = schema;
214
+ const valid = ajv.validate(schemaBody, data);
215
+ if (valid) {
216
+ return { valid: true, errors: [], warnings: [] };
217
+ }
218
+ const errors = (ajv.errors ?? []).map((err) => {
219
+ const path = err.instancePath || "/";
220
+ return `${path}: ${err.message}`;
221
+ });
222
+ return { valid: false, errors, warnings: [] };
223
+ }
224
+
225
+ // cli/color.ts
226
+ function parseColor(color2) {
227
+ const hex = color2.match(/^#([0-9a-f]{3,8})$/i);
228
+ if (hex) {
229
+ let h = hex[1];
230
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
231
+ return [
232
+ parseInt(h.slice(0, 2), 16),
233
+ parseInt(h.slice(2, 4), 16),
234
+ parseInt(h.slice(4, 6), 16)
235
+ ];
236
+ }
237
+ const rgb = color2.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
238
+ if (rgb) {
239
+ return [parseInt(rgb[1]), parseInt(rgb[2]), parseInt(rgb[3])];
240
+ }
241
+ const oklch = color2.match(
242
+ /oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)/
243
+ );
244
+ if (oklch) {
245
+ const L = oklch[2] === "%" ? parseFloat(oklch[1]) / 100 : parseFloat(oklch[1]);
246
+ const C = parseFloat(oklch[3]);
247
+ const H = parseFloat(oklch[4]);
248
+ return oklchToSrgb(L, C, H);
249
+ }
250
+ return null;
251
+ }
252
+ function oklchToSrgb(L, C, H) {
253
+ const hRad = H * Math.PI / 180;
254
+ const a = C * Math.cos(hRad);
255
+ const b = C * Math.sin(hRad);
256
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
257
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
258
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
259
+ const l = l_ * l_ * l_;
260
+ const m = m_ * m_ * m_;
261
+ const s = s_ * s_ * s_;
262
+ const rl = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
263
+ const gl = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
264
+ const bl = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
265
+ const gamma = (x) => x <= 31308e-7 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
266
+ const clamp = (x) => Math.max(0, Math.min(255, Math.round(x * 255)));
267
+ return [clamp(gamma(rl)), clamp(gamma(gl)), clamp(gamma(bl))];
268
+ }
269
+ function relativeLuminance(r, g, b) {
270
+ const [rs, gs, bs] = [r, g, b].map((c) => {
271
+ const s = c / 255;
272
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
273
+ });
274
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
275
+ }
276
+ function getContrastRatio(color1, color2) {
277
+ const c1 = parseColor(color1);
278
+ const c2 = parseColor(color2);
279
+ if (!c1 || !c2) return 21;
280
+ const l1 = relativeLuminance(...c1);
281
+ const l2 = relativeLuminance(...c2);
282
+ const lighter = Math.max(l1, l2);
283
+ const darker = Math.min(l1, l2);
284
+ return (lighter + 0.05) / (darker + 0.05);
285
+ }
286
+ function findColorValue(colors, name) {
287
+ const token = colors[name];
288
+ if (isToken(token)) return token.$value;
289
+ if (typeof token === "object" && token !== null) {
290
+ const nested = token;
291
+ if (isToken(nested)) return nested.$value;
292
+ }
293
+ return null;
294
+ }
295
+ function isToken(v) {
296
+ return typeof v === "object" && v !== null && "$value" in v;
297
+ }
298
+
299
+ // cli/lint.ts
300
+ var requiredFields = (spec) => {
301
+ const results = [];
302
+ if (!spec.meta.version) {
303
+ results.push({
304
+ rule: "meta/version-required",
305
+ severity: "warning",
306
+ message: "meta.version is recommended for tracking changes",
307
+ path: "meta.version"
308
+ });
309
+ }
310
+ if (!spec.core) {
311
+ results.push({
312
+ rule: "core/missing",
313
+ severity: "warning",
314
+ message: "core section is missing \u2014 brand identity is undefined",
315
+ path: "core"
316
+ });
317
+ } else {
318
+ if (!spec.core.essence && !spec.core.tagline) {
319
+ results.push({
320
+ rule: "core/identity-missing",
321
+ severity: "warning",
322
+ message: "Neither essence nor tagline is defined",
323
+ path: "core"
324
+ });
325
+ }
326
+ if (!spec.core.personality || spec.core.personality.length === 0) {
327
+ results.push({
328
+ rule: "core/personality-empty",
329
+ severity: "info",
330
+ message: "No personality traits defined",
331
+ path: "core.personality"
332
+ });
333
+ }
334
+ if (!spec.core.voice) {
335
+ results.push({
336
+ rule: "core/voice-missing",
337
+ severity: "info",
338
+ message: "Voice guidelines not defined",
339
+ path: "core.voice"
340
+ });
341
+ }
342
+ }
343
+ if (!spec.tokens) {
344
+ results.push({
345
+ rule: "tokens/missing",
346
+ severity: "warning",
347
+ message: "No design tokens defined",
348
+ path: "tokens"
349
+ });
350
+ } else {
351
+ if (!spec.tokens.colors || Object.keys(spec.tokens.colors).length === 0) {
352
+ results.push({
353
+ rule: "tokens/colors-empty",
354
+ severity: "warning",
355
+ message: "No color tokens defined",
356
+ path: "tokens.colors"
357
+ });
358
+ }
359
+ if (!spec.tokens.typography || Object.keys(spec.tokens.typography).length === 0) {
360
+ results.push({
361
+ rule: "tokens/typography-empty",
362
+ severity: "info",
363
+ message: "No typography tokens defined",
364
+ path: "tokens.typography"
365
+ });
366
+ }
367
+ }
368
+ return results;
369
+ };
370
+ var colorContrast = (spec) => {
371
+ var _a;
372
+ const results = [];
373
+ const colors = (_a = spec.tokens) == null ? void 0 : _a.colors;
374
+ if (!colors) return results;
375
+ const bg = findColorValue(colors, "background");
376
+ const fg = findColorValue(colors, "foreground");
377
+ if (bg && fg) {
378
+ const ratio = getContrastRatio(bg, fg);
379
+ if (ratio < 4.5) {
380
+ results.push({
381
+ rule: "contrast/bg-fg-aa",
382
+ severity: "error",
383
+ message: `Background/foreground contrast ratio is ${ratio.toFixed(1)}:1 (WCAG AA requires 4.5:1)`,
384
+ path: "tokens.colors"
385
+ });
386
+ } else if (ratio < 7) {
387
+ results.push({
388
+ rule: "contrast/bg-fg-aaa",
389
+ severity: "info",
390
+ message: `Background/foreground contrast ratio is ${ratio.toFixed(1)}:1 (WCAG AAA requires 7:1)`,
391
+ path: "tokens.colors"
392
+ });
393
+ }
394
+ }
395
+ const primary = findColorValue(colors, "primary");
396
+ if (primary && bg) {
397
+ const ratio = getContrastRatio(bg, primary);
398
+ if (ratio < 3) {
399
+ results.push({
400
+ rule: "contrast/primary-bg",
401
+ severity: "warning",
402
+ message: `Primary on background contrast ratio is ${ratio.toFixed(1)}:1 (minimum 3:1 for large text)`,
403
+ path: "tokens.colors.primary"
404
+ });
405
+ }
406
+ }
407
+ return results;
408
+ };
409
+ var ASSET_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+)+$/;
410
+ var assetNaming = (spec) => {
411
+ const results = [];
412
+ if (!spec.assets) return results;
413
+ for (const asset of spec.assets) {
414
+ const fileName = asset.file.split("/").pop() ?? asset.file;
415
+ if (!ASSET_PATTERN.test(fileName)) {
416
+ results.push({
417
+ rule: "assets/naming-convention",
418
+ severity: "warning",
419
+ message: `Asset "${asset.file}" doesn't follow {role}-{variant}.{ext} naming convention`,
420
+ path: "assets"
421
+ });
422
+ }
423
+ if (!asset.role) {
424
+ results.push({
425
+ rule: "assets/role-missing",
426
+ severity: "info",
427
+ message: `Asset "${asset.file}" has no role defined`,
428
+ path: "assets"
429
+ });
430
+ }
431
+ }
432
+ return results;
433
+ };
434
+ var KEBAB_CASE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
435
+ var tokenNaming = (spec) => {
436
+ const results = [];
437
+ if (!spec.tokens) return results;
438
+ for (const [group, tokens] of Object.entries(spec.tokens)) {
439
+ if (!tokens || typeof tokens !== "object") continue;
440
+ for (const name of Object.keys(tokens)) {
441
+ if (name.startsWith("$")) continue;
442
+ if (!KEBAB_CASE.test(name)) {
443
+ results.push({
444
+ rule: "tokens/naming-kebab",
445
+ severity: "info",
446
+ message: `Token "${group}.${name}" should use kebab-case`,
447
+ path: `tokens.${group}.${name}`
448
+ });
449
+ }
450
+ }
451
+ }
452
+ return results;
453
+ };
454
+ var essentialColors = (spec) => {
455
+ var _a;
456
+ const results = [];
457
+ const colors = (_a = spec.tokens) == null ? void 0 : _a.colors;
458
+ if (!colors) return results;
459
+ const essential = ["primary", "background", "foreground"];
460
+ for (const name of essential) {
461
+ if (!findColorValue(colors, name)) {
462
+ results.push({
463
+ rule: "tokens/essential-color",
464
+ severity: "warning",
465
+ message: `Essential color token "${name}" is missing`,
466
+ path: `tokens.colors.${name}`
467
+ });
468
+ }
469
+ }
470
+ return results;
471
+ };
472
+ var ALL_RULES = [
473
+ requiredFields,
474
+ colorContrast,
475
+ assetNaming,
476
+ tokenNaming,
477
+ essentialColors
478
+ ];
479
+ function lintBrandspec(spec) {
480
+ const results = ALL_RULES.flatMap((rule) => rule(spec));
481
+ const errors = results.filter((r) => r.severity === "error").length;
482
+ const warnings = results.filter((r) => r.severity === "warning").length;
483
+ const infos = results.filter((r) => r.severity === "info").length;
484
+ const score = Math.max(
485
+ 0,
486
+ Math.min(100, 100 - errors * 10 - warnings * 3 - infos)
487
+ );
488
+ return { score, results, errors, warnings, infos };
489
+ }
490
+
491
+ // cli/tokens.ts
492
+ function toCss(data) {
493
+ var _a;
494
+ const lines = [];
495
+ if (!data.tokens) return ":root {}\n";
496
+ for (const [group, tokens] of Object.entries(data.tokens)) {
497
+ if (!tokens) continue;
498
+ for (const [name, token] of Object.entries(tokens)) {
499
+ const prefix = group === "colors" ? "" : `${group}-`;
500
+ const varName = `--${prefix}${name}`;
501
+ lines.push(` ${varName}: ${token.$value};`);
502
+ }
503
+ }
504
+ const darkLines = [];
505
+ if (data.tokens.colors) {
506
+ for (const [name, token] of Object.entries(data.tokens.colors)) {
507
+ const dark = (_a = token.$extensions) == null ? void 0 : _a["dark"];
508
+ if (typeof dark === "string") {
509
+ darkLines.push(` --${name}: ${dark};`);
510
+ }
511
+ }
512
+ }
513
+ let output = `:root {
514
+ ${lines.join("\n")}
515
+ }
516
+ `;
517
+ if (darkLines.length > 0) {
518
+ output += `
519
+ .dark {
520
+ ${darkLines.join("\n")}
521
+ }
522
+ `;
523
+ }
524
+ return output;
525
+ }
526
+ function toTailwindCss(data) {
527
+ const lines = [];
528
+ if (!data.tokens) return '@import "tailwindcss";\n\n@theme {}\n';
529
+ if (data.tokens.colors) {
530
+ for (const [name, token] of Object.entries(data.tokens.colors)) {
531
+ lines.push(` --color-${name}: ${token.$value};`);
532
+ }
533
+ }
534
+ if (data.tokens.typography) {
535
+ for (const [name, token] of Object.entries(data.tokens.typography)) {
536
+ lines.push(` --font-${name}: ${token.$value};`);
537
+ }
538
+ }
539
+ if (data.tokens.spacing) {
540
+ for (const [name, token] of Object.entries(data.tokens.spacing)) {
541
+ lines.push(` --spacing-${name}: ${token.$value};`);
542
+ }
543
+ }
544
+ if (data.tokens.radius) {
545
+ for (const [name, token] of Object.entries(data.tokens.radius)) {
546
+ lines.push(` --radius-${name}: ${token.$value};`);
547
+ }
548
+ }
549
+ return `@import "tailwindcss";
550
+
551
+ @theme {
552
+ ${lines.join("\n")}
553
+ }
554
+ `;
555
+ }
556
+ function toFigmaTokens(data) {
557
+ if (!data.tokens) return JSON.stringify({}, null, 2);
558
+ const output = {};
559
+ for (const [group, tokens] of Object.entries(data.tokens)) {
560
+ if (!tokens) continue;
561
+ output[group] = {};
562
+ for (const [name, token] of Object.entries(tokens)) {
563
+ output[group][name] = {
564
+ value: token.$value,
565
+ type: token.$type ?? group,
566
+ ...token.$description && { description: token.$description }
567
+ };
568
+ }
569
+ }
570
+ return JSON.stringify(output, null, 2) + "\n";
571
+ }
572
+ function toStyleDictionary(data) {
573
+ const tokenFile = {};
574
+ if (data.tokens) {
575
+ for (const [group, tokens2] of Object.entries(data.tokens)) {
576
+ if (!tokens2) continue;
577
+ tokenFile[group] = {};
578
+ for (const [name, token] of Object.entries(tokens2)) {
579
+ tokenFile[group][name] = {
580
+ $value: token.$value,
581
+ $type: token.$type ?? group,
582
+ ...token.$description && { $description: token.$description }
583
+ };
584
+ }
585
+ }
586
+ }
587
+ const tokens = JSON.stringify(tokenFile, null, 2) + "\n";
588
+ const brandName = data.meta.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
589
+ const config = {
590
+ source: ["tokens.json"],
591
+ usesDtcg: true,
592
+ platforms: {
593
+ css: {
594
+ transformGroup: "css",
595
+ buildPath: `build/css/`,
596
+ files: [
597
+ {
598
+ destination: "variables.css",
599
+ format: "css/variables",
600
+ options: { outputReferences: true }
601
+ }
602
+ ]
603
+ },
604
+ scss: {
605
+ transformGroup: "scss",
606
+ buildPath: `build/scss/`,
607
+ files: [
608
+ {
609
+ destination: "_variables.scss",
610
+ format: "scss/variables",
611
+ options: { outputReferences: true }
612
+ }
613
+ ]
614
+ },
615
+ ios: {
616
+ transformGroup: "ios-swift",
617
+ buildPath: `build/ios/`,
618
+ files: [
619
+ {
620
+ destination: `${brandName}.swift`,
621
+ format: "ios-swift/class.swift",
622
+ className: data.meta.name.replace(/[^a-zA-Z0-9]/g, "")
623
+ }
624
+ ]
625
+ },
626
+ android: {
627
+ transformGroup: "android",
628
+ buildPath: `build/android/`,
629
+ files: [
630
+ {
631
+ destination: "colors.xml",
632
+ format: "android/resources",
633
+ filter: { $type: "color" }
634
+ },
635
+ {
636
+ destination: "dimens.xml",
637
+ format: "android/resources",
638
+ filter: { $type: "dimension" }
639
+ }
640
+ ]
641
+ }
642
+ }
643
+ };
644
+ return { tokens, config: JSON.stringify(config, null, 2) + "\n" };
645
+ }
646
+
647
+ // cli/remote.ts
648
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
649
+ import { resolve, dirname, join } from "path";
650
+ import { homedir } from "os";
651
+ var API_BASE = process.env.BRANDSPEC_API_URL ?? "https://brandspec.tools";
652
+ function getCredentialsPath() {
653
+ return join(homedir(), ".config", "brandspec", "credentials");
654
+ }
655
+ function loadToken() {
656
+ const envToken = process.env.BRANDSPEC_TOKEN;
657
+ if (envToken) return envToken;
658
+ const credPath = getCredentialsPath();
659
+ if (existsSync(credPath)) {
660
+ const content = readFileSync(credPath, "utf-8").trim();
661
+ if (content) return content;
662
+ }
663
+ return null;
664
+ }
665
+ function parseOrgBrand(str) {
666
+ const parts = str.split("/");
667
+ if (parts.length !== 2) return null;
668
+ const [org, brand] = parts;
669
+ const slugRe = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
670
+ if (!slugRe.test(org) || !slugRe.test(brand)) return null;
671
+ return { org, brand };
672
+ }
673
+ function loadRemote(args) {
674
+ const positional = args.find((a) => !a.startsWith("-") && a.includes("/"));
675
+ if (positional) return parseOrgBrand(positional);
676
+ const rcPath = resolve(".brandspecrc");
677
+ if (existsSync(rcPath)) {
678
+ const content = readFileSync(rcPath, "utf-8");
679
+ const match = content.match(/remote:\s*(.+)/);
680
+ if (match) return parseOrgBrand(match[1].trim());
681
+ }
682
+ return null;
683
+ }
684
+ function ensureBrandspecrc(org, brand) {
685
+ const rcPath = resolve(".brandspecrc");
686
+ if (!existsSync(rcPath)) {
687
+ writeFileSync(rcPath, `remote: ${org}/${brand}
688
+ `, "utf-8");
689
+ }
690
+ }
691
+ function saveCredentials(token) {
692
+ const credPath = getCredentialsPath();
693
+ mkdirSync(dirname(credPath), { recursive: true });
694
+ writeFileSync(credPath, token, { mode: 384 });
695
+ }
696
+
697
+ // cli/cli.ts
698
+ var VERSION = "0.1.0";
699
+ function color(code, text) {
700
+ if (process.env.NO_COLOR || !process.stdout.isTTY) return text;
701
+ return `\x1B[${code}m${text}\x1B[0m`;
702
+ }
703
+ var green = (t) => color("32", t);
704
+ var yellow = (t) => color("33", t);
705
+ var red = (t) => color("31", t);
706
+ var dim = (t) => color("2", t);
707
+ var __filename = fileURLToPath(import.meta.url);
708
+ var __dirname = dirname2(__filename);
709
+ function requireToken() {
710
+ const token = loadToken();
711
+ if (!token) {
712
+ console.error("Not logged in. Run 'brandspec login' first or set BRANDSPEC_TOKEN.");
713
+ process.exit(1);
714
+ }
715
+ return token;
716
+ }
717
+ function requireRemote(args) {
718
+ const remote = loadRemote(args);
719
+ if (!remote) {
720
+ console.error("No remote specified. Use 'brandspec push org/brand' or create .brandspecrc.");
721
+ process.exit(1);
722
+ }
723
+ return remote;
724
+ }
725
+ var HELP = `
726
+ brandspec v${VERSION}
727
+ Define Brand Identity as code.
728
+
729
+ Usage:
730
+ brandspec Lint brand.yaml in current directory
731
+ brandspec <command> [options]
732
+
733
+ Commands:
734
+ init Create a brandspec/ directory with templates
735
+ lint [path] Lint a brand.yaml (validate + rules + score)
736
+ --json Output as JSON (for CI/pipe)
737
+ --quiet Exit code only, no output
738
+ validate [path] Alias for lint
739
+ generate [path] Generate token files from brand.yaml
740
+ --format <fmt> css, tailwind, figma, sd, all (comma-separated)
741
+ --out <dir> Output directory (default: output/ next to brand.yaml)
742
+
743
+ consult [path] Print brand context for AI consultation
744
+
745
+ workshop start Print start prompt for AI workshop
746
+ workshop resume Print resume prompt for AI workshop
747
+ workshop status Show current workshop position
748
+
749
+ login Save API token for brandspec.tools
750
+ logout Remove saved API token
751
+ pull [org/brand] Pull brand from brandspec.tools
752
+ --no-workshop Exclude _workshop/ files
753
+ push [org/brand] Push brand to brandspec.tools
754
+
755
+ Options:
756
+ --help, -h Show this help
757
+ --version, -v Show version
758
+ `.trim();
759
+ var MINIMAL_TEMPLATE = {
760
+ meta: {
761
+ name: "My Brand",
762
+ version: "0.1.0",
763
+ updated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
764
+ },
765
+ core: {
766
+ essence: "",
767
+ tagline: "",
768
+ personality: [],
769
+ voice: {
770
+ tone: [],
771
+ principles: []
772
+ }
773
+ },
774
+ tokens: {
775
+ colors: {
776
+ primary: {
777
+ $value: "oklch(0.65 0.18 250)",
778
+ $type: "color",
779
+ $description: "Primary brand color"
780
+ },
781
+ "primary-foreground": {
782
+ $value: "oklch(0.98 0.01 250)",
783
+ $type: "color"
784
+ },
785
+ background: {
786
+ $value: "oklch(0.99 0.005 250)",
787
+ $type: "color"
788
+ },
789
+ foreground: {
790
+ $value: "oklch(0.15 0.02 250)",
791
+ $type: "color"
792
+ }
793
+ },
794
+ typography: {
795
+ heading: {
796
+ $value: "Inter, system-ui, sans-serif",
797
+ $type: "fontFamily"
798
+ },
799
+ body: {
800
+ $value: "Inter, system-ui, sans-serif",
801
+ $type: "fontFamily"
802
+ }
803
+ }
804
+ }
805
+ };
806
+ function resolveBrandYaml(filePath) {
807
+ let target = resolve2(filePath ?? "brand.yaml");
808
+ if (existsSync2(target) && statSync(target).isDirectory()) {
809
+ target = join2(target, "brand.yaml");
810
+ }
811
+ return target;
812
+ }
813
+ function cmdInit() {
814
+ const targetDir = resolve2("brandspec");
815
+ if (existsSync2(targetDir)) {
816
+ console.error("brandspec/ already exists. cd brandspec to start working.");
817
+ process.exit(1);
818
+ }
819
+ const templatesDir = getWorkshopTemplatesDir();
820
+ if (!existsSync2(templatesDir)) {
821
+ console.error("Templates not found. Ensure brandspec is installed correctly.");
822
+ process.exit(1);
823
+ }
824
+ cpSync(templatesDir, targetDir, { recursive: true });
825
+ const positionPath = join2(targetDir, "_workshop", "position.yml");
826
+ if (existsSync2(positionPath)) {
827
+ let pos = readFileSync2(positionPath, "utf-8");
828
+ pos = pos.replace('updated: ""', `updated: "${(/* @__PURE__ */ new Date()).toISOString()}"`);
829
+ writeFileSync2(positionPath, pos, "utf-8");
830
+ }
831
+ const clipCmd = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -sel c";
832
+ console.log("Created brandspec/");
833
+ console.log();
834
+ console.log("Next steps:");
835
+ console.log(" cd brandspec");
836
+ console.log();
837
+ console.log(" # Start the brand workshop with any AI:");
838
+ console.log(` npx brandspec workshop start | ${clipCmd}`);
839
+ console.log(" npx brandspec workshop start > prompt.txt # or save to file");
840
+ console.log();
841
+ console.log(" # Or edit brand.yaml directly, then:");
842
+ console.log(" npx brandspec generate --format tailwind");
843
+ }
844
+ function formatLintReport(report) {
845
+ const lines = [];
846
+ const errors = report.results.filter((r) => r.severity === "error");
847
+ const warnings = report.results.filter((r) => r.severity === "warning");
848
+ const infos = report.results.filter((r) => r.severity === "info");
849
+ if (errors.length > 0) {
850
+ lines.push("");
851
+ lines.push(red(`Errors (${errors.length}):`));
852
+ for (const r of errors) {
853
+ lines.push(red(` \u2717 [${r.rule}] ${r.message}`));
854
+ }
855
+ }
856
+ if (warnings.length > 0) {
857
+ lines.push("");
858
+ lines.push(yellow(`Warnings (${warnings.length}):`));
859
+ for (const r of warnings) {
860
+ lines.push(yellow(` \u26A0 [${r.rule}] ${r.message}`));
861
+ }
862
+ }
863
+ if (infos.length > 0) {
864
+ lines.push("");
865
+ lines.push(dim(`Info (${infos.length}):`));
866
+ for (const r of infos) {
867
+ lines.push(dim(` \u2139 [${r.rule}] ${r.message}`));
868
+ }
869
+ }
870
+ return lines.join("\n");
871
+ }
872
+ function cmdLint(args) {
873
+ const jsonMode = args.includes("--json");
874
+ const quietMode = args.includes("--quiet");
875
+ const filePath = args.find((a) => !a.startsWith("-"));
876
+ const target = resolveBrandYaml(filePath);
877
+ if (!existsSync2(target)) {
878
+ if (jsonMode) {
879
+ console.log(JSON.stringify({ error: `File not found: ${target}` }));
880
+ } else if (!quietMode) {
881
+ console.error(`File not found: ${target}`);
882
+ console.error("Run 'brandspec init' to create one, or specify a path.");
883
+ }
884
+ process.exit(1);
885
+ }
886
+ const content = readFileSync2(target, "utf-8");
887
+ const parseResult = parse(content);
888
+ if (!parseResult.success) {
889
+ if (jsonMode) {
890
+ console.log(JSON.stringify({ error: "parse", details: parseResult.errors }));
891
+ } else if (!quietMode) {
892
+ console.error(`Parse errors in ${target}:`);
893
+ for (const err of parseResult.errors) {
894
+ console.error(` - ${err}`);
895
+ }
896
+ }
897
+ process.exit(1);
898
+ }
899
+ const validationResult = validate(parseResult.data);
900
+ if (!validationResult.valid) {
901
+ if (jsonMode) {
902
+ console.log(JSON.stringify({ error: "schema", details: validationResult.errors }));
903
+ } else if (!quietMode) {
904
+ console.error(`Schema validation errors in ${target}:`);
905
+ for (const err of validationResult.errors) {
906
+ console.error(` - ${err}`);
907
+ }
908
+ }
909
+ process.exit(1);
910
+ }
911
+ const data = parseResult.data;
912
+ const report = lintBrandspec(data);
913
+ if (jsonMode) {
914
+ console.log(JSON.stringify({
915
+ name: data.meta.name,
916
+ score: report.score,
917
+ errors: report.errors,
918
+ warnings: report.warnings,
919
+ infos: report.infos,
920
+ results: report.results
921
+ }));
922
+ } else if (!quietMode) {
923
+ const scoreText = `${report.score}/100`;
924
+ const coloredScore = report.score >= 80 ? green(scoreText) : report.score >= 50 ? yellow(scoreText) : red(scoreText);
925
+ console.log(`${data.meta.name} \u2014 Score: ${coloredScore}`);
926
+ console.log(formatLintReport(report));
927
+ }
928
+ if (report.errors > 0) {
929
+ process.exit(1);
930
+ }
931
+ }
932
+ function cmdValidate(args) {
933
+ if (!args.includes("--json") && !args.includes("--quiet")) {
934
+ console.error("Hint: 'validate' now runs 'lint'. Use 'brandspec lint' directly.");
935
+ }
936
+ cmdLint(args);
937
+ }
938
+ var VALID_FORMATS = ["css", "tailwind", "figma", "sd", "all"];
939
+ function parseFormats(input) {
940
+ const parts = input.split(",").map((s) => s.trim());
941
+ for (const p of parts) {
942
+ if (!VALID_FORMATS.includes(p)) {
943
+ console.error(`Unknown format: ${p}`);
944
+ console.error(`Valid formats: ${VALID_FORMATS.join(", ")}`);
945
+ process.exit(1);
946
+ }
947
+ }
948
+ if (parts.includes("all")) return ["all"];
949
+ return parts;
950
+ }
951
+ function detectProjectFormat() {
952
+ const checks = [
953
+ { file: "tailwind.config.ts", format: "tailwind", hint: "Tailwind project detected" },
954
+ { file: "tailwind.config.js", format: "tailwind", hint: "Tailwind project detected" },
955
+ { file: "postcss.config.js", format: "css", hint: "PostCSS project detected" },
956
+ { file: "postcss.config.mjs", format: "css", hint: "PostCSS project detected" }
957
+ ];
958
+ const parentDir = resolve2("..");
959
+ for (const check of checks) {
960
+ if (existsSync2(join2(parentDir, check.file))) {
961
+ return check.format;
962
+ }
963
+ }
964
+ return void 0;
965
+ }
966
+ function cmdGenerate(args) {
967
+ let filePath;
968
+ let formatArg;
969
+ let outDir;
970
+ for (let i = 0; i < args.length; i++) {
971
+ if (args[i] === "--format" && args[i + 1]) {
972
+ formatArg = args[++i];
973
+ } else if (args[i] === "--out" && args[i + 1]) {
974
+ outDir = args[++i];
975
+ } else if (!args[i].startsWith("-")) {
976
+ filePath = args[i];
977
+ }
978
+ }
979
+ let formats;
980
+ if (formatArg) {
981
+ formats = parseFormats(formatArg);
982
+ } else {
983
+ const detected = detectProjectFormat();
984
+ if (detected) {
985
+ console.log(`Detected ${detected} project. Use --format all to generate everything.`);
986
+ formats = [detected];
987
+ } else {
988
+ formats = ["all"];
989
+ }
990
+ }
991
+ const target = resolveBrandYaml(filePath);
992
+ if (!existsSync2(target)) {
993
+ console.error(`File not found: ${target}`);
994
+ console.error("Run 'brandspec init' to create one, or specify a path.");
995
+ process.exit(1);
996
+ }
997
+ const content = readFileSync2(target, "utf-8");
998
+ const parseResult = parse(content);
999
+ if (!parseResult.success) {
1000
+ console.error(`Parse errors in ${target}:`);
1001
+ for (const err of parseResult.errors) {
1002
+ console.error(` - ${err}`);
1003
+ }
1004
+ process.exit(1);
1005
+ }
1006
+ const data = parseResult.data;
1007
+ const out = resolve2(outDir ?? join2(dirname2(target), "output"));
1008
+ mkdirSync2(out, { recursive: true });
1009
+ const generated = [];
1010
+ const shouldGenerate = (f) => formats.includes(f) || formats.includes("all");
1011
+ if (shouldGenerate("css")) {
1012
+ const dest = resolve2(out, "tokens.css");
1013
+ writeFileSync2(dest, toCss(data), "utf-8");
1014
+ generated.push("tokens.css");
1015
+ }
1016
+ if (shouldGenerate("tailwind")) {
1017
+ const dest = resolve2(out, "theme.css");
1018
+ writeFileSync2(dest, toTailwindCss(data), "utf-8");
1019
+ generated.push("theme.css");
1020
+ }
1021
+ if (shouldGenerate("figma")) {
1022
+ const dest = resolve2(out, "figma-tokens.json");
1023
+ writeFileSync2(dest, toFigmaTokens(data), "utf-8");
1024
+ generated.push("figma-tokens.json");
1025
+ }
1026
+ if (shouldGenerate("sd")) {
1027
+ const sdDir = resolve2(out, "style-dictionary");
1028
+ mkdirSync2(sdDir, { recursive: true });
1029
+ const sd = toStyleDictionary(data);
1030
+ writeFileSync2(resolve2(sdDir, "tokens.json"), sd.tokens, "utf-8");
1031
+ writeFileSync2(resolve2(sdDir, "config.json"), sd.config, "utf-8");
1032
+ generated.push("style-dictionary/tokens.json");
1033
+ generated.push("style-dictionary/config.json");
1034
+ }
1035
+ console.log(`Generated from ${data.meta.name}:`);
1036
+ for (const f of generated) {
1037
+ console.log(` ${out}/${f}`);
1038
+ }
1039
+ }
1040
+ function getWorkshopTemplatesDir() {
1041
+ return resolve2(__dirname, "..", "workshop", "templates");
1042
+ }
1043
+ function getWorkshopDir() {
1044
+ return resolve2(__dirname, "..", "workshop");
1045
+ }
1046
+ function ensureWorkshopState() {
1047
+ const positionPath = resolve2("_workshop", "position.yml");
1048
+ const decisionsPath = resolve2("_workshop", "decisions.yml");
1049
+ if (!existsSync2(positionPath) || !existsSync2(decisionsPath)) {
1050
+ console.error("No _workshop/ state found in current directory.");
1051
+ console.error("Run 'brandspec init' first, then cd into brandspec/.");
1052
+ process.exit(1);
1053
+ }
1054
+ return { positionPath, decisionsPath };
1055
+ }
1056
+ function isInitialPosition(position) {
1057
+ return /phase:\s*1/.test(position) && /completed_at:\s*null/.test(position);
1058
+ }
1059
+ function cmdWorkshopStart() {
1060
+ const { positionPath, decisionsPath } = ensureWorkshopState();
1061
+ const position = readFileSync2(positionPath, "utf-8");
1062
+ const decisions = readFileSync2(decisionsPath, "utf-8");
1063
+ const hasDecisions = !/decisions:\s*\[\]/.test(decisions);
1064
+ if (!isInitialPosition(position) || hasDecisions) {
1065
+ console.error("Workshop already in progress \u2014 resuming.");
1066
+ cmdWorkshopResume();
1067
+ return;
1068
+ }
1069
+ const workshopDir = getWorkshopDir();
1070
+ const skillMd = readFileSync2(join2(workshopDir, "SKILL.md"), "utf-8");
1071
+ const flowMd = readFileSync2(join2(workshopDir, "flow.md"), "utf-8");
1072
+ const phase1 = readFileSync2(join2(workshopDir, "phases", "01-discovery.md"), "utf-8");
1073
+ const lines = [];
1074
+ lines.push("# brandspec workshop \u2014 Start Session");
1075
+ lines.push("");
1076
+ lines.push("You are a brand identity facilitator. Guide the user through the brandspec workshop.");
1077
+ lines.push("First ask the user's preferred session language, then proceed with Phase 1: Discovery.");
1078
+ lines.push("");
1079
+ lines.push("---");
1080
+ lines.push("");
1081
+ lines.push(skillMd);
1082
+ lines.push("");
1083
+ lines.push("---");
1084
+ lines.push("");
1085
+ lines.push(flowMd);
1086
+ lines.push("");
1087
+ lines.push("---");
1088
+ lines.push("");
1089
+ lines.push("## Current Phase Guide");
1090
+ lines.push("");
1091
+ lines.push(phase1);
1092
+ lines.push("");
1093
+ lines.push("---");
1094
+ lines.push("");
1095
+ lines.push("Begin the workshop now. Ask the user their preferred session language.");
1096
+ console.log(lines.join("\n"));
1097
+ }
1098
+ function cmdWorkshopStatus() {
1099
+ const positionPath = resolve2("_workshop", "position.yml");
1100
+ if (!existsSync2(positionPath)) {
1101
+ console.error("No _workshop/position.yml found in current directory.");
1102
+ console.error("Are you inside a brandspec project?");
1103
+ process.exit(1);
1104
+ }
1105
+ const content = readFileSync2(positionPath, "utf-8");
1106
+ console.log("Workshop status:");
1107
+ console.log(content);
1108
+ }
1109
+ function getPhaseFile(phase) {
1110
+ const phaseFiles = {
1111
+ 1: "01-discovery.md",
1112
+ 2: "02-concept.md",
1113
+ 3: "03-visual.md",
1114
+ 4: "04-documentation.md"
1115
+ };
1116
+ const file = phaseFiles[phase];
1117
+ if (!file) return "";
1118
+ const filePath = join2(getWorkshopDir(), "phases", file);
1119
+ return existsSync2(filePath) ? readFileSync2(filePath, "utf-8") : "";
1120
+ }
1121
+ function cmdWorkshopResume() {
1122
+ const { positionPath, decisionsPath } = ensureWorkshopState();
1123
+ const position = readFileSync2(positionPath, "utf-8");
1124
+ const decisions = readFileSync2(decisionsPath, "utf-8");
1125
+ const phaseMatch = position.match(/phase:\s*(\d+)/);
1126
+ const currentPhase = phaseMatch ? parseInt(phaseMatch[1], 10) : 1;
1127
+ const workshopDir = getWorkshopDir();
1128
+ const skillMd = readFileSync2(join2(workshopDir, "SKILL.md"), "utf-8");
1129
+ const phaseGuide = getPhaseFile(currentPhase);
1130
+ const lines = [];
1131
+ lines.push("# brandspec workshop \u2014 Resume Session");
1132
+ lines.push("");
1133
+ lines.push("You are a brand identity facilitator. The user is resuming a workshop session.");
1134
+ lines.push("Restore context from the state below, then continue where they left off.");
1135
+ lines.push("");
1136
+ lines.push("---");
1137
+ lines.push("");
1138
+ lines.push(skillMd);
1139
+ lines.push("");
1140
+ lines.push("---");
1141
+ lines.push("");
1142
+ lines.push("## Current State");
1143
+ lines.push("");
1144
+ lines.push("### Position");
1145
+ lines.push("```yaml");
1146
+ lines.push(position.trim());
1147
+ lines.push("```");
1148
+ lines.push("");
1149
+ lines.push("### Decisions");
1150
+ lines.push("```yaml");
1151
+ lines.push(decisions.trim());
1152
+ lines.push("```");
1153
+ lines.push("");
1154
+ const memoPath = resolve2("_workshop", "memo.md");
1155
+ if (existsSync2(memoPath)) {
1156
+ const memo = readFileSync2(memoPath, "utf-8").trim();
1157
+ if (memo) {
1158
+ lines.push("### Working Notes");
1159
+ lines.push(memo);
1160
+ lines.push("");
1161
+ }
1162
+ }
1163
+ lines.push("---");
1164
+ lines.push("");
1165
+ lines.push(`## Current Phase Guide (Phase ${currentPhase})`);
1166
+ lines.push("");
1167
+ if (phaseGuide) {
1168
+ lines.push(phaseGuide);
1169
+ }
1170
+ lines.push("");
1171
+ lines.push("---");
1172
+ lines.push("");
1173
+ lines.push("Resume the workshop. Present the restored state summary, then continue from the current step.");
1174
+ console.log(lines.join("\n"));
1175
+ }
1176
+ function cmdWorkshop(args) {
1177
+ const sub = args[0];
1178
+ if (!sub || sub === "--help") {
1179
+ console.log(`
1180
+ brandspec workshop \u2014 AI-facilitated brand creation
1181
+
1182
+ Commands:
1183
+ start Print start prompt for AI (initial state only)
1184
+ resume Print resume prompt for AI (any state)
1185
+ status Show current workshop position
1186
+ `.trim());
1187
+ process.exit(0);
1188
+ }
1189
+ switch (sub) {
1190
+ case "start":
1191
+ cmdWorkshopStart();
1192
+ break;
1193
+ case "status":
1194
+ cmdWorkshopStatus();
1195
+ break;
1196
+ case "resume":
1197
+ cmdWorkshopResume();
1198
+ break;
1199
+ default:
1200
+ console.error(`Unknown workshop command: ${sub}`);
1201
+ console.error("");
1202
+ console.error("Available commands:");
1203
+ console.error(" start Print start prompt for AI (initial state only)");
1204
+ console.error(" resume Print resume prompt for AI (any state)");
1205
+ console.error(" status Show current workshop position");
1206
+ process.exit(1);
1207
+ }
1208
+ }
1209
+ function cmdConsult(args) {
1210
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
1211
+ const filePath = args.find((a) => !a.startsWith("-"));
1212
+ const target = resolveBrandYaml(filePath);
1213
+ if (!existsSync2(target)) {
1214
+ console.error(`File not found: ${target}`);
1215
+ console.error("Run 'brandspec init' to create one, or specify a path.");
1216
+ process.exit(1);
1217
+ }
1218
+ const content = readFileSync2(target, "utf-8");
1219
+ const parseResult = parse(content);
1220
+ if (!parseResult.success) {
1221
+ console.error(`Parse errors in ${target}:`);
1222
+ for (const err of parseResult.errors) {
1223
+ console.error(` - ${err}`);
1224
+ }
1225
+ process.exit(1);
1226
+ }
1227
+ const data = parseResult.data;
1228
+ const lines = [];
1229
+ lines.push(`# You are a brand consultant for ${data.meta.name}.`);
1230
+ lines.push("");
1231
+ lines.push("## Your Role");
1232
+ lines.push(
1233
+ "You evaluate business decisions, creative work, and communications against this brand's identity. When asked about any business decision, advertisement, campaign, copy, or design \u2014 assess whether it aligns with the brand. Flag inconsistencies and suggest alternatives."
1234
+ );
1235
+ lines.push("");
1236
+ lines.push("## Brand Identity");
1237
+ lines.push(`- Name: ${data.meta.name}`);
1238
+ if (data.meta.description) lines.push(`- Description: ${data.meta.description}`);
1239
+ if ((_a = data.core) == null ? void 0 : _a.essence) lines.push(`- Essence: ${data.core.essence}`);
1240
+ if ((_b = data.core) == null ? void 0 : _b.tagline) lines.push(`- Tagline: ${data.core.tagline}`);
1241
+ if ((_c = data.core) == null ? void 0 : _c.mission) lines.push(`- Mission: ${data.core.mission}`);
1242
+ if ((_d = data.core) == null ? void 0 : _d.vision) lines.push(`- Vision: ${data.core.vision}`);
1243
+ if ((_f = (_e = data.core) == null ? void 0 : _e.values) == null ? void 0 : _f.length) lines.push(`- Values: ${data.core.values.join(", ")}`);
1244
+ if ((_h = (_g = data.core) == null ? void 0 : _g.personality) == null ? void 0 : _h.length)
1245
+ lines.push(`- Personality: ${data.core.personality.join(", ")}`);
1246
+ lines.push("");
1247
+ if ((_i = data.core) == null ? void 0 : _i.voice) {
1248
+ const v = data.core.voice;
1249
+ lines.push("## Voice & Tone");
1250
+ if ((_j = v.tone) == null ? void 0 : _j.length) lines.push(`Tone: ${v.tone.join(", ")}`);
1251
+ if ((_k = v.principles) == null ? void 0 : _k.length) {
1252
+ lines.push("Principles:");
1253
+ for (const p of v.principles) lines.push(`- ${p}`);
1254
+ }
1255
+ lines.push("");
1256
+ }
1257
+ const hasColors = ((_l = data.tokens) == null ? void 0 : _l.colors) && Object.keys(data.tokens.colors).length > 0;
1258
+ const hasTypography = ((_m = data.tokens) == null ? void 0 : _m.typography) && Object.keys(data.tokens.typography).length > 0;
1259
+ const hasAssets = data.assets && data.assets.length > 0;
1260
+ if (hasColors || hasTypography || hasAssets) {
1261
+ lines.push("## Visual Identity");
1262
+ if (hasColors) {
1263
+ lines.push("Colors:");
1264
+ for (const [name, token] of Object.entries(data.tokens.colors)) {
1265
+ if (token.$description) {
1266
+ const hex = (_o = (_n = token.$extensions) == null ? void 0 : _n.compat) == null ? void 0 : _o.hex;
1267
+ lines.push(`- ${name}: ${token.$description}${hex ? ` (${hex})` : ""}`);
1268
+ }
1269
+ }
1270
+ }
1271
+ if (hasTypography) {
1272
+ lines.push("Typography:");
1273
+ for (const [name, token] of Object.entries(data.tokens.typography)) {
1274
+ const desc = token.$description ? ` \u2014 ${token.$description}` : "";
1275
+ lines.push(`- ${name}: ${token.$value}${desc}`);
1276
+ }
1277
+ }
1278
+ if (hasAssets) {
1279
+ const roles = new Set(data.assets.map((a) => a.role).filter(Boolean));
1280
+ const parts = [];
1281
+ if (roles.has("symbol")) parts.push("symbol");
1282
+ if (roles.has("wordmark")) parts.push("wordmark");
1283
+ if (roles.has("logo")) parts.push("logo");
1284
+ if (roles.has("favicon")) parts.push("favicon");
1285
+ if (parts.length) {
1286
+ lines.push(`Logo system: ${parts.join(" + ")}`);
1287
+ }
1288
+ for (const asset of data.assets) {
1289
+ if (asset.description) {
1290
+ lines.push(`- ${asset.id ?? asset.file}: ${asset.description}`);
1291
+ }
1292
+ }
1293
+ }
1294
+ lines.push("");
1295
+ }
1296
+ if (data.guidelines && Object.keys(data.guidelines).length > 0) {
1297
+ lines.push("## Guidelines");
1298
+ for (const [, section] of Object.entries(data.guidelines)) {
1299
+ if (section.content) {
1300
+ lines.push(section.content.trim());
1301
+ lines.push("");
1302
+ }
1303
+ if ((_p = section.rules) == null ? void 0 : _p.length) {
1304
+ for (const rule of section.rules) {
1305
+ lines.push(`- [${rule.severity}] ${rule.description}`);
1306
+ }
1307
+ lines.push("");
1308
+ }
1309
+ }
1310
+ }
1311
+ if (data.extensions) {
1312
+ const extKeys = Object.keys(data.extensions);
1313
+ if (extKeys.length > 0) {
1314
+ lines.push("## Additional Context");
1315
+ for (const key of extKeys) {
1316
+ const ext = data.extensions[key];
1317
+ if (ext && typeof ext === "object" && !Array.isArray(ext)) {
1318
+ const record = ext;
1319
+ const stringEntries = Object.entries(record).filter(
1320
+ ([, v]) => typeof v === "string"
1321
+ );
1322
+ if (stringEntries.length > 0) {
1323
+ lines.push(`### ${key}`);
1324
+ for (const [k, v] of stringEntries) {
1325
+ lines.push(`- ${k}: ${v}`);
1326
+ }
1327
+ lines.push("");
1328
+ }
1329
+ }
1330
+ }
1331
+ }
1332
+ }
1333
+ lines.push("---");
1334
+ lines.push(
1335
+ "Use this prompt as a system message for any AI model to get brand-aligned consultation."
1336
+ );
1337
+ console.log(lines.join("\n"));
1338
+ }
1339
+ async function cmdLogin(args) {
1340
+ let token;
1341
+ const tokenIdx = args.indexOf("--token");
1342
+ if (tokenIdx !== -1 && args[tokenIdx + 1]) {
1343
+ token = args[tokenIdx + 1];
1344
+ }
1345
+ if (!token) {
1346
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1347
+ token = await new Promise((resolve3) => {
1348
+ rl.question("API token: ", (answer) => {
1349
+ rl.close();
1350
+ resolve3(answer.trim());
1351
+ });
1352
+ });
1353
+ }
1354
+ if (!token || !token.startsWith("bst_")) {
1355
+ console.error("Invalid token. Token must start with 'bst_'.");
1356
+ process.exit(1);
1357
+ }
1358
+ saveCredentials(token);
1359
+ console.log(`Token saved to ${getCredentialsPath()}`);
1360
+ }
1361
+ function cmdLogout() {
1362
+ const credPath = getCredentialsPath();
1363
+ if (existsSync2(credPath)) {
1364
+ unlinkSync(credPath);
1365
+ console.log("Logged out. Token removed.");
1366
+ } else {
1367
+ console.log("No saved token found.");
1368
+ }
1369
+ }
1370
+ function collectFiles(dir, base) {
1371
+ const results = [];
1372
+ if (!existsSync2(dir)) return results;
1373
+ const entries = readdirSync(dir, { withFileTypes: true });
1374
+ for (const entry of entries) {
1375
+ const fullPath = join2(dir, entry.name);
1376
+ const relPath = join2(base, entry.name);
1377
+ if (entry.isDirectory()) {
1378
+ results.push(...collectFiles(fullPath, relPath));
1379
+ } else {
1380
+ results.push({ path: relPath, data: readFileSync2(fullPath) });
1381
+ }
1382
+ }
1383
+ return results;
1384
+ }
1385
+ async function cmdPull(args) {
1386
+ const token = requireToken();
1387
+ const remote = requireRemote(args);
1388
+ const excludeWorkshop = args.includes("--no-workshop");
1389
+ const url = `${API_BASE}/api/v1/${remote.org}/${remote.brand}/pull${excludeWorkshop ? "" : "?include_workshop=true"}`;
1390
+ const res = await fetch(url, {
1391
+ headers: { Authorization: `Bearer ${token}` }
1392
+ });
1393
+ if (!res.ok) {
1394
+ const body = await res.text();
1395
+ console.error(`Pull failed (${res.status}): ${body}`);
1396
+ process.exit(1);
1397
+ }
1398
+ const arrayBuf = await res.arrayBuffer();
1399
+ const JSZip = (await import("jszip")).default;
1400
+ const zip = await JSZip.loadAsync(arrayBuf);
1401
+ const outDir = resolve2("brandspec");
1402
+ mkdirSync2(outDir, { recursive: true });
1403
+ for (const [filePath, zipEntry] of Object.entries(zip.files)) {
1404
+ if (zipEntry.dir) {
1405
+ mkdirSync2(join2(outDir, filePath), { recursive: true });
1406
+ } else {
1407
+ const dest = join2(outDir, filePath);
1408
+ mkdirSync2(dirname2(dest), { recursive: true });
1409
+ const content = await zipEntry.async("nodebuffer");
1410
+ writeFileSync2(dest, content);
1411
+ }
1412
+ }
1413
+ ensureBrandspecrc(remote.org, remote.brand);
1414
+ const fileCount = Object.values(zip.files).filter((f) => !f.dir).length;
1415
+ console.log(`Pulled ${remote.org}/${remote.brand} \u2192 brandspec/ (${fileCount} files)`);
1416
+ }
1417
+ async function cmdPush(args) {
1418
+ var _a, _b;
1419
+ const token = requireToken();
1420
+ const remote = requireRemote(args);
1421
+ const yamlPath = resolve2("brand.yaml");
1422
+ if (!existsSync2(yamlPath)) {
1423
+ console.error("brand.yaml not found in current directory.");
1424
+ process.exit(1);
1425
+ }
1426
+ const yamlContent = readFileSync2(yamlPath, "utf-8");
1427
+ const formData = new FormData();
1428
+ formData.append("yaml", new Blob([yamlContent], { type: "text/yaml" }), "brand.yaml");
1429
+ const assetsDir = resolve2("assets");
1430
+ const assetFiles = collectFiles(assetsDir, "");
1431
+ for (const file of assetFiles) {
1432
+ formData.append("assets", new Blob([new Uint8Array(file.data)]), file.path);
1433
+ }
1434
+ const workshopDir = resolve2("_workshop");
1435
+ const workshopFiles = collectFiles(workshopDir, "");
1436
+ for (const file of workshopFiles) {
1437
+ formData.append("workshop", new Blob([new Uint8Array(file.data)]), file.path);
1438
+ }
1439
+ const url = `${API_BASE}/api/v1/${remote.org}/${remote.brand}/push`;
1440
+ const res = await fetch(url, {
1441
+ method: "POST",
1442
+ headers: { Authorization: `Bearer ${token}` },
1443
+ body: formData
1444
+ });
1445
+ if (!res.ok) {
1446
+ const body = await res.text();
1447
+ console.error(`Push failed (${res.status}): ${body}`);
1448
+ process.exit(1);
1449
+ }
1450
+ const result = await res.json();
1451
+ console.log(`Pushed to ${remote.org}/${remote.brand}: ${result.action ?? "ok"}`);
1452
+ if ((_a = result.warnings) == null ? void 0 : _a.length) {
1453
+ for (const w of result.warnings) {
1454
+ console.warn(` warn: ${w}`);
1455
+ }
1456
+ }
1457
+ if ((_b = result.assetErrors) == null ? void 0 : _b.length) {
1458
+ for (const e of result.assetErrors) {
1459
+ console.error(` asset error: ${e}`);
1460
+ }
1461
+ }
1462
+ ensureBrandspecrc(remote.org, remote.brand);
1463
+ }
1464
+ async function main() {
1465
+ const args = process.argv.slice(2);
1466
+ const command = args[0];
1467
+ if (!command || command === "--help" || command === "-h") {
1468
+ if (!command && existsSync2(resolveBrandYaml())) {
1469
+ cmdLint([]);
1470
+ return;
1471
+ }
1472
+ console.log(HELP);
1473
+ process.exit(0);
1474
+ }
1475
+ if (command === "--version" || command === "-v") {
1476
+ console.log(VERSION);
1477
+ process.exit(0);
1478
+ }
1479
+ switch (command) {
1480
+ case "init":
1481
+ cmdInit();
1482
+ break;
1483
+ case "lint":
1484
+ cmdLint(args.slice(1));
1485
+ break;
1486
+ case "validate":
1487
+ cmdValidate(args.slice(1));
1488
+ break;
1489
+ case "generate":
1490
+ cmdGenerate(args.slice(1));
1491
+ break;
1492
+ case "workshop":
1493
+ cmdWorkshop(args.slice(1));
1494
+ break;
1495
+ case "consult":
1496
+ cmdConsult(args.slice(1));
1497
+ break;
1498
+ case "login":
1499
+ await cmdLogin(args.slice(1));
1500
+ break;
1501
+ case "logout":
1502
+ cmdLogout();
1503
+ break;
1504
+ case "pull":
1505
+ await cmdPull(args.slice(1));
1506
+ break;
1507
+ case "push":
1508
+ await cmdPush(args.slice(1));
1509
+ break;
1510
+ default:
1511
+ console.error(`Unknown command: ${command}`);
1512
+ console.log(HELP);
1513
+ process.exit(1);
1514
+ }
1515
+ }
1516
+ main();