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/LICENSE +21 -0
- package/README.md +230 -0
- package/dist/cli.js +1516 -0
- package/dist/index.cjs +773 -0
- package/dist/index.d.cts +402 -0
- package/dist/index.d.ts +402 -0
- package/dist/index.js +718 -0
- package/package.json +61 -0
- package/schema/spec/assets.md +219 -0
- package/schema/spec/core.md +203 -0
- package/schema/spec/guidelines.md +110 -0
- package/schema/spec/tokens.md +389 -0
- package/schema/v0.1.0.yaml +201 -0
- package/workshop/SKILL.md +218 -0
- package/workshop/flow.md +181 -0
- package/workshop/phases/01-discovery.md +156 -0
- package/workshop/phases/02-concept.md +234 -0
- package/workshop/phases/03-visual.md +271 -0
- package/workshop/phases/04-documentation.md +99 -0
- package/workshop/templates/_workshop/decisions.yml +15 -0
- package/workshop/templates/_workshop/memo.md +23 -0
- package/workshop/templates/_workshop/position.yml +9 -0
- package/workshop/templates/_workshop/session.md +28 -0
- package/workshop/templates/brand.yaml +112 -0
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();
|