brand-intent 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -12
- package/brands/krume/krume.brand +54 -34
- package/cli/index.mjs +365 -90
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -29,23 +29,15 @@ Brand Intent is a set of five composable file types that encode brand knowledge
|
|
|
29
29
|
Syntax highlighting, autocomplete, and validation for all five file types.
|
|
30
30
|
|
|
31
31
|
```
|
|
32
|
-
ext install
|
|
32
|
+
ext install studiostoll.brand-intent-lang
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
### 2.
|
|
35
|
+
### 2. Run init
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
npx brand-intent install-skills
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Works with Claude Code, Cursor, Gemini CLI, VS Code Copilot, Codex CLI, and any MCP-compatible agent.
|
|
42
|
-
|
|
43
|
-
### 3. Author your brand
|
|
44
|
-
|
|
45
|
-
Start with `.identity` (strategy, voice, audiences), then derive `.brand` (expression intent). Add formats, purposes, and compositions as needed. The Krume bakery brand in `brands/krume/` is a fully commented reference implementation.
|
|
37
|
+
The CLI walks you through everything: explore the Krume bakery reference, or create your own brand from scratch. It also offers to install AI skills at the end.
|
|
46
38
|
|
|
47
39
|
```bash
|
|
48
|
-
npx brand-intent init
|
|
40
|
+
npx brand-intent init
|
|
49
41
|
```
|
|
50
42
|
|
|
51
43
|
## Documentation
|
package/brands/krume/krume.brand
CHANGED
|
@@ -27,10 +27,15 @@ theme Laden
|
|
|
27
27
|
text-primary: $primary
|
|
28
28
|
text-secondary: $secondary
|
|
29
29
|
text-tertiary: $accent1
|
|
30
|
+
text-subtle: #A89282
|
|
31
|
+
text-muted: #C4B5A7
|
|
30
32
|
logo: $primary
|
|
33
|
+
sticker-bg: $accent1
|
|
34
|
+
sticker-fg: $primary
|
|
31
35
|
cta: $accent2
|
|
32
36
|
label-bg: $accent1
|
|
33
37
|
label-fg: $primary
|
|
38
|
+
icon: $secondary
|
|
34
39
|
divider: $accent1
|
|
35
40
|
|
|
36
41
|
theme Tafel
|
|
@@ -38,33 +43,61 @@ theme Tafel
|
|
|
38
43
|
text-primary: $white
|
|
39
44
|
text-secondary: #D4C4B0
|
|
40
45
|
text-tertiary: #8A7A68
|
|
46
|
+
text-subtle: #6A5A48
|
|
47
|
+
text-muted: #4A3A28
|
|
41
48
|
logo: $accent1
|
|
49
|
+
sticker-bg: $accent2
|
|
50
|
+
sticker-fg: $white
|
|
42
51
|
cta: $accent1
|
|
43
52
|
label-bg: $accent2
|
|
44
53
|
label-fg: $white
|
|
54
|
+
icon: #D4C4B0
|
|
45
55
|
divider: $accent1
|
|
46
56
|
|
|
47
57
|
# ── Typography ──
|
|
48
|
-
#
|
|
49
|
-
#
|
|
58
|
+
# Hanken Grotesk: clean, geometric, contemporary.
|
|
59
|
+
# ABC Stefan Simple: display face for hero moments.
|
|
50
60
|
|
|
51
61
|
font primary
|
|
52
|
-
name:
|
|
62
|
+
name: Hanken Grotesk
|
|
53
63
|
fallback: sans-serif
|
|
54
64
|
source: google
|
|
55
65
|
|
|
66
|
+
font secondary
|
|
67
|
+
name: ABC Stefan Simple
|
|
68
|
+
fallback: sans-serif
|
|
69
|
+
source: local
|
|
70
|
+
|
|
71
|
+
typography display
|
|
72
|
+
font: $secondary
|
|
73
|
+
weight: 400
|
|
74
|
+
size: 8
|
|
75
|
+
lineHeight: 1.1
|
|
76
|
+
autofit-min: 5
|
|
77
|
+
autofit-max: 12
|
|
78
|
+
|
|
56
79
|
typography headline
|
|
57
80
|
weight: 700
|
|
58
81
|
strong: 800
|
|
59
|
-
size: 6
|
|
82
|
+
size: 6
|
|
60
83
|
lineHeight: 1.15
|
|
61
84
|
autofit-min: 4
|
|
62
85
|
autofit-max: 10
|
|
63
86
|
|
|
64
87
|
typography kicker
|
|
65
|
-
weight:
|
|
66
|
-
size:
|
|
88
|
+
weight: 400
|
|
89
|
+
size: 2.6
|
|
67
90
|
lineHeight: 1.3
|
|
91
|
+
autofit-min: 2.5
|
|
92
|
+
autofit-max: 3.5
|
|
93
|
+
|
|
94
|
+
typography quote
|
|
95
|
+
weight: 700
|
|
96
|
+
strong: 800
|
|
97
|
+
size: 5.5
|
|
98
|
+
lineHeight: 1.2
|
|
99
|
+
autofit-min: 4
|
|
100
|
+
autofit-max: 8
|
|
68
101
|
|
|
69
102
|
typography body
|
|
70
103
|
weight: 400
|
|
@@ -76,6 +109,12 @@ typography caption
|
|
|
76
109
|
size: 2.2
|
|
77
110
|
lineHeight: 1.4
|
|
78
111
|
|
|
112
|
+
typography attribution
|
|
113
|
+
weight: 300
|
|
114
|
+
size: 2.2
|
|
115
|
+
lineHeight: 1.4
|
|
116
|
+
letterSpacing: 0.08
|
|
117
|
+
|
|
79
118
|
typography label
|
|
80
119
|
weight: 600
|
|
81
120
|
size: 2
|
|
@@ -83,10 +122,10 @@ typography label
|
|
|
83
122
|
uppercase
|
|
84
123
|
|
|
85
124
|
typography number
|
|
86
|
-
weight:
|
|
87
|
-
size:
|
|
88
|
-
lineHeight:
|
|
89
|
-
letterSpacing:
|
|
125
|
+
weight: 300
|
|
126
|
+
size: 14
|
|
127
|
+
lineHeight: 0.9
|
|
128
|
+
letterSpacing: -0.03
|
|
90
129
|
|
|
91
130
|
# ── Spacing ──
|
|
92
131
|
|
|
@@ -105,31 +144,12 @@ divider line
|
|
|
105
144
|
width: 100%
|
|
106
145
|
color: divider
|
|
107
146
|
align: left
|
|
108
|
-
spacing:
|
|
147
|
+
spacing: s
|
|
109
148
|
|
|
110
|
-
divider
|
|
111
|
-
thickness: 0.
|
|
112
|
-
width:
|
|
149
|
+
divider ruler
|
|
150
|
+
thickness: 0.8
|
|
151
|
+
width: 20%
|
|
113
152
|
color: divider
|
|
114
153
|
align: left
|
|
115
|
-
spacing:
|
|
154
|
+
spacing: m
|
|
116
155
|
|
|
117
|
-
# ── Voice Constraints ──
|
|
118
|
-
# Operationalized from identity voice reasoning.
|
|
119
|
-
# These are enforceable limits, not restated strategy.
|
|
120
|
-
# Derived from: persona is direct, rhythm is short sentences.
|
|
121
|
-
|
|
122
|
-
voice-constraints
|
|
123
|
-
register: informal
|
|
124
|
-
sentence-max: 8 # words - short, direct, like the baker talks
|
|
125
|
-
headline-pattern: noun-first
|
|
126
|
-
number-format: cardinal # "drei", not "3"
|
|
127
|
-
|
|
128
|
-
# ── Content Defaults ──
|
|
129
|
-
# Derived from identity: this is a small bakery, not a media brand.
|
|
130
|
-
# Low density. Natural images. Simple dividers.
|
|
131
|
-
|
|
132
|
-
content-defaults
|
|
133
|
-
density: low
|
|
134
|
-
image-treatment: natural
|
|
135
|
-
divider: line
|
package/cli/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
4
4
|
import { join, dirname, resolve, basename } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createInterface } from 'readline';
|
|
6
7
|
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = dirname(__filename);
|
|
@@ -12,152 +13,426 @@ const SKILL_FILE = join(PKG_ROOT, 'skills', 'brand-intent.md');
|
|
|
12
13
|
|
|
13
14
|
// ── Harness detection and install paths ──
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
claude:
|
|
17
|
-
cursor:
|
|
18
|
-
gemini:
|
|
19
|
-
copilot: '.github/copilot-instructions',
|
|
20
|
-
|
|
16
|
+
const HARNESSES = [
|
|
17
|
+
{ id: 'claude', label: 'Claude Code', dir: '.claude/commands' },
|
|
18
|
+
{ id: 'cursor', label: 'Cursor', dir: '.cursor/rules' },
|
|
19
|
+
{ id: 'gemini', label: 'Gemini CLI', dir: '.gemini' },
|
|
20
|
+
{ id: 'copilot', label: 'VS Code Copilot', dir: '.github/copilot-instructions' },
|
|
21
|
+
];
|
|
21
22
|
|
|
22
23
|
function detectHarnesses(cwd) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
return HARNESSES.filter(h => {
|
|
25
|
+
const parent = h.dir.split('/')[0];
|
|
26
|
+
return existsSync(join(cwd, parent));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Readline helpers (single shared instance) ──
|
|
31
|
+
|
|
32
|
+
let _rl;
|
|
33
|
+
function rl() {
|
|
34
|
+
if (!_rl) {
|
|
35
|
+
_rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
30
36
|
}
|
|
31
|
-
return
|
|
37
|
+
return _rl;
|
|
38
|
+
}
|
|
39
|
+
function closeRl() {
|
|
40
|
+
if (_rl) { _rl.close(); _rl = null; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ask(q) {
|
|
44
|
+
return new Promise((res) => rl().question(q, res));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function choose(prompt, options) {
|
|
48
|
+
console.log(` ${prompt}\n`);
|
|
49
|
+
options.forEach((opt, i) => {
|
|
50
|
+
console.log(` ${i + 1}) ${opt.label}`);
|
|
51
|
+
});
|
|
52
|
+
console.log('');
|
|
53
|
+
const answer = await ask(' Choose [1-' + options.length + ']: ');
|
|
54
|
+
const idx = parseInt(answer, 10) - 1;
|
|
55
|
+
if (idx >= 0 && idx < options.length) return options[idx].value;
|
|
56
|
+
return options[0].value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function confirm(question, defaultYes = true) {
|
|
60
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
61
|
+
const answer = await ask(` ${question} (${hint}) `);
|
|
62
|
+
if (answer.trim() === '') return defaultYes;
|
|
63
|
+
return answer.trim().toLowerCase().startsWith('y');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function input(question, defaultValue) {
|
|
67
|
+
const hint = defaultValue ? ` (${defaultValue})` : '';
|
|
68
|
+
const answer = await ask(` ${question}${hint}: `);
|
|
69
|
+
return answer.trim() || defaultValue || '';
|
|
32
70
|
}
|
|
33
71
|
|
|
34
|
-
// ──
|
|
72
|
+
// ── File operations ──
|
|
73
|
+
|
|
74
|
+
function copyDirRecursive(src, dest) {
|
|
75
|
+
mkdirSync(dest, { recursive: true });
|
|
76
|
+
for (const entry of readdirSync(src)) {
|
|
77
|
+
const srcPath = join(src, entry);
|
|
78
|
+
const destPath = join(dest, entry);
|
|
79
|
+
if (statSync(srcPath).isDirectory()) {
|
|
80
|
+
copyDirRecursive(srcPath, destPath);
|
|
81
|
+
} else {
|
|
82
|
+
copyFileSync(srcPath, destPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
35
86
|
|
|
36
|
-
function installSkills(cwd) {
|
|
87
|
+
async function installSkills(cwd) {
|
|
37
88
|
if (!existsSync(SKILL_FILE)) {
|
|
38
89
|
console.error('Error: skill file not found at', SKILL_FILE);
|
|
39
90
|
process.exit(1);
|
|
40
91
|
}
|
|
41
92
|
|
|
42
93
|
const skill = readFileSync(SKILL_FILE, 'utf-8');
|
|
43
|
-
|
|
94
|
+
let harnesses = detectHarnesses(cwd);
|
|
44
95
|
|
|
45
96
|
if (harnesses.length === 0) {
|
|
46
|
-
//
|
|
47
|
-
|
|
97
|
+
// Nothing detected — ask which to install
|
|
98
|
+
console.log(' Which AI tools do you use?\n');
|
|
99
|
+
HARNESSES.forEach((h, i) => {
|
|
100
|
+
console.log(` ${i + 1}) ${h.label}`);
|
|
101
|
+
});
|
|
102
|
+
console.log(` ${HARNESSES.length + 1}) all of the above`);
|
|
103
|
+
console.log('');
|
|
104
|
+
const answer = await ask(` Choose [1-${HARNESSES.length + 1}]: `);
|
|
105
|
+
const idx = parseInt(answer, 10) - 1;
|
|
106
|
+
if (idx === HARNESSES.length) {
|
|
107
|
+
harnesses = HARNESSES;
|
|
108
|
+
} else if (idx >= 0 && idx < HARNESSES.length) {
|
|
109
|
+
harnesses = [HARNESSES[idx]];
|
|
110
|
+
} else {
|
|
111
|
+
harnesses = [HARNESSES[0]];
|
|
112
|
+
}
|
|
48
113
|
}
|
|
49
114
|
|
|
50
|
-
|
|
51
|
-
for (const { name, dir } of harnesses) {
|
|
115
|
+
for (const { label, dir } of harnesses) {
|
|
52
116
|
const target = join(cwd, dir);
|
|
53
117
|
mkdirSync(target, { recursive: true });
|
|
54
118
|
const dest = join(target, 'brand-intent.md');
|
|
55
119
|
writeFileSync(dest, skill, 'utf-8');
|
|
56
|
-
console.log(`
|
|
57
|
-
installed++;
|
|
120
|
+
console.log(` \u2713 ${label} \u2014 ${dir}/brand-intent.md`);
|
|
58
121
|
}
|
|
59
|
-
|
|
60
|
-
console.log(`\nInstalled Brand Intent skill to ${installed} harness${installed > 1 ? 'es' : ''}.`);
|
|
61
122
|
}
|
|
62
123
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
124
|
+
// ── Explore the example ──
|
|
125
|
+
|
|
126
|
+
function scaffoldExample(targetDir) {
|
|
127
|
+
const brandSrc = join(PKG_ROOT, 'brands', 'krume');
|
|
128
|
+
const exampleDest = join(targetDir, 'examples', 'krume');
|
|
129
|
+
|
|
130
|
+
// Copy brand files
|
|
131
|
+
copyDirRecursive(brandSrc, exampleDest);
|
|
132
|
+
console.log(' \u2713 examples/krume/krume.identity');
|
|
133
|
+
console.log(' \u2713 examples/krume/krume.brand');
|
|
134
|
+
|
|
135
|
+
// Copy one format, one purpose, one composition as starting examples
|
|
136
|
+
const examples = [
|
|
137
|
+
{ layer: 'formats', file: 'instagram-4-5-feed-portrait.format' },
|
|
138
|
+
{ layer: 'purposes', file: 'daily-bread.purpose' },
|
|
139
|
+
{ layer: 'compositions', file: 'editorial.composition' },
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
for (const { layer, file } of examples) {
|
|
143
|
+
const src = join(PKG_ROOT, layer, file);
|
|
144
|
+
if (existsSync(src)) {
|
|
145
|
+
const dest = join(exampleDest, layer, file);
|
|
146
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
147
|
+
copyFileSync(src, dest);
|
|
148
|
+
console.log(` \u2713 examples/krume/${layer}/${file}`);
|
|
72
149
|
}
|
|
73
150
|
}
|
|
151
|
+
|
|
152
|
+
console.log(`
|
|
153
|
+
The Krume bakery is a fully commented reference implementation.
|
|
154
|
+
Read the .identity file first \u2014 it explains every decision.
|
|
155
|
+
Then look at how .brand derives expression intent from identity.
|
|
156
|
+
Formats, purposes, and compositions show the remaining three layers.`);
|
|
74
157
|
}
|
|
75
158
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
159
|
+
// ── Create your own brand ──
|
|
160
|
+
|
|
161
|
+
async function scaffoldOwnBrand(targetDir) {
|
|
162
|
+
console.log('\n Let\u2019s define your brand. Answer what you can \u2014 skip the rest.\n');
|
|
163
|
+
|
|
164
|
+
const brandName = await input('Brand name');
|
|
165
|
+
if (!brandName) {
|
|
166
|
+
console.error('\n A brand needs a name. Try again.\n');
|
|
81
167
|
process.exit(1);
|
|
82
168
|
}
|
|
83
169
|
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
170
|
+
const brandId = brandName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
171
|
+
const language = await input('Language', 'en');
|
|
172
|
+
const locale = await input('Locale', language === 'de' ? 'de-DE' : language === 'en' ? 'en-US' : `${language}-${language.toUpperCase()}`);
|
|
173
|
+
|
|
174
|
+
// Identity
|
|
175
|
+
const essence = await input('Essence \u2014 one sentence, what this brand is about');
|
|
176
|
+
const promise = await input('Promise \u2014 what people experience');
|
|
177
|
+
const tagline = await input('Tagline');
|
|
178
|
+
const archetype = await input('Archetype (e.g. craftsman, sage, rebel)');
|
|
179
|
+
|
|
180
|
+
// Voice
|
|
181
|
+
console.log('\n Voice \u2014 how the brand speaks.\n');
|
|
182
|
+
const register = await input('Register (e.g. informal/Du, formal/Sie, casual)');
|
|
183
|
+
const persona = await input('Persona \u2014 who is speaking?');
|
|
184
|
+
const rhythm = await input('Rhythm \u2014 sentence style');
|
|
185
|
+
|
|
186
|
+
console.log('\n Always rules \u2014 what the brand always does. One per line, empty line to stop.\n');
|
|
187
|
+
const always = await collectList();
|
|
188
|
+
|
|
189
|
+
console.log('\n Never rules \u2014 what the brand never does. One per line, empty line to stop.\n');
|
|
190
|
+
const never = await collectList();
|
|
191
|
+
|
|
192
|
+
// Pillars
|
|
193
|
+
console.log('');
|
|
194
|
+
const primaryPillars = await input('Primary pillars (comma-separated)');
|
|
195
|
+
const secondaryPillars = await input('Secondary pillars (comma-separated)');
|
|
196
|
+
const avoidPillars = await input('Avoid topics (comma-separated)');
|
|
197
|
+
|
|
198
|
+
// Audiences
|
|
199
|
+
console.log('\n Audiences \u2014 who the brand speaks to.\n');
|
|
200
|
+
const audiences = await collectAudiences();
|
|
201
|
+
|
|
202
|
+
// Colors
|
|
203
|
+
console.log('\n Colors \u2014 the brand palette.\n');
|
|
204
|
+
const primaryColor = await input('Primary color (hex)', '#1a1a1a');
|
|
205
|
+
const secondaryColor = await input('Secondary color (hex)', '#4a4a4a');
|
|
206
|
+
const accentColor = await input('Accent color (hex)', '#0066cc');
|
|
207
|
+
const whiteColor = await input('Background/white (hex)', '#ffffff');
|
|
208
|
+
|
|
209
|
+
// Typography
|
|
210
|
+
console.log('\n Typography\n');
|
|
211
|
+
const primaryFont = await input('Primary font name', 'Inter');
|
|
212
|
+
const primaryFontSource = await input('Font source (google/local)', 'google');
|
|
213
|
+
|
|
214
|
+
// ── Write .identity ──
|
|
215
|
+
|
|
216
|
+
let identity = `# ${brandId}.identity\n`;
|
|
217
|
+
if (essence) identity += `\nessence: ${essence}\n`;
|
|
218
|
+
if (promise) identity += `\npromise: ${promise}\n`;
|
|
219
|
+
if (tagline) identity += `\ntagline: ${tagline}\n`;
|
|
220
|
+
if (archetype) identity += `\narchetype: ${archetype}\n`;
|
|
221
|
+
|
|
222
|
+
if (register || persona || rhythm || always.length || never.length) {
|
|
223
|
+
identity += `\nvoice\n`;
|
|
224
|
+
if (register) identity += ` register: ${register}\n`;
|
|
225
|
+
if (persona) identity += ` persona: ${persona}\n`;
|
|
226
|
+
if (rhythm) identity += ` rhythm: ${rhythm}\n`;
|
|
227
|
+
if (always.length) {
|
|
228
|
+
identity += `\n always:\n`;
|
|
229
|
+
for (const rule of always) identity += ` - ${rule}\n`;
|
|
230
|
+
}
|
|
231
|
+
if (never.length) {
|
|
232
|
+
identity += `\n never:\n`;
|
|
233
|
+
for (const rule of never) identity += ` - ${rule}\n`;
|
|
96
234
|
}
|
|
97
235
|
}
|
|
98
236
|
|
|
99
|
-
|
|
237
|
+
if (primaryPillars || secondaryPillars || avoidPillars) {
|
|
238
|
+
identity += `\npillars\n`;
|
|
239
|
+
if (primaryPillars) identity += ` primary: ${primaryPillars}\n`;
|
|
240
|
+
if (secondaryPillars) identity += ` secondary: ${secondaryPillars}\n`;
|
|
241
|
+
if (avoidPillars) identity += ` avoid: ${avoidPillars}\n`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const aud of audiences) {
|
|
245
|
+
identity += `\naudience ${aud.id}\n`;
|
|
246
|
+
identity += ` label: ${aud.label}\n`;
|
|
247
|
+
if (aud.profile) identity += ` profile: ${aud.profile}\n`;
|
|
248
|
+
if (aud.motivation) identity += ` motivation: ${aud.motivation}\n`;
|
|
249
|
+
if (aud.language) identity += ` language: ${aud.language}\n`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
writeFileSync(join(targetDir, `${brandId}.identity`), identity, 'utf-8');
|
|
253
|
+
console.log(`\n \u2713 ${brandId}.identity`);
|
|
254
|
+
|
|
255
|
+
// ── Write .brand ──
|
|
256
|
+
|
|
257
|
+
let brand = `# ${brandId}.brand \u2014 expression intent derived from ${brandId}.identity\n\n`;
|
|
258
|
+
brand += `id: ${brandId}\n`;
|
|
259
|
+
brand += `name: ${brandName}\n`;
|
|
260
|
+
brand += `language: ${language}\n`;
|
|
261
|
+
brand += `locale: ${locale}\n`;
|
|
262
|
+
|
|
263
|
+
brand += `\nbrand-colors\n`;
|
|
264
|
+
brand += ` primary: ${primaryColor}\n`;
|
|
265
|
+
brand += ` secondary: ${secondaryColor}\n`;
|
|
266
|
+
brand += ` accent1: ${accentColor}\n`;
|
|
267
|
+
brand += ` white: ${whiteColor}\n`;
|
|
268
|
+
|
|
269
|
+
brand += `\ntheme Default\n`;
|
|
270
|
+
brand += ` background: $white\n`;
|
|
271
|
+
brand += ` text-primary: $primary\n`;
|
|
272
|
+
brand += ` text-secondary: $secondary\n`;
|
|
273
|
+
brand += ` cta: $accent1\n`;
|
|
274
|
+
brand += ` divider: $secondary\n`;
|
|
275
|
+
|
|
276
|
+
brand += `\nfont primary\n`;
|
|
277
|
+
brand += ` name: ${primaryFont}\n`;
|
|
278
|
+
brand += ` fallback: sans-serif\n`;
|
|
279
|
+
brand += ` source: ${primaryFontSource}\n`;
|
|
280
|
+
|
|
281
|
+
brand += `\ntypography headline\n`;
|
|
282
|
+
brand += ` weight: 700\n`;
|
|
283
|
+
brand += ` size: 6\n`;
|
|
284
|
+
brand += ` lineHeight: 1.15\n`;
|
|
285
|
+
|
|
286
|
+
brand += `\ntypography body\n`;
|
|
287
|
+
brand += ` weight: 400\n`;
|
|
288
|
+
brand += ` size: 2.8\n`;
|
|
289
|
+
brand += ` lineHeight: 1.5\n`;
|
|
290
|
+
|
|
291
|
+
brand += `\ntypography caption\n`;
|
|
292
|
+
brand += ` weight: 400\n`;
|
|
293
|
+
brand += ` size: 2.2\n`;
|
|
294
|
+
brand += ` lineHeight: 1.4\n`;
|
|
295
|
+
|
|
296
|
+
brand += `\ntypography label\n`;
|
|
297
|
+
brand += ` weight: 600\n`;
|
|
298
|
+
brand += ` size: 2\n`;
|
|
299
|
+
brand += ` lineHeight: 1.2\n`;
|
|
300
|
+
brand += ` uppercase\n`;
|
|
301
|
+
|
|
302
|
+
brand += `\nspacing:\n`;
|
|
303
|
+
brand += ` unit: cqmin\n`;
|
|
304
|
+
brand += ` xs: 1.5\n`;
|
|
305
|
+
brand += ` s: 2\n`;
|
|
306
|
+
brand += ` m: 3\n`;
|
|
307
|
+
brand += ` l: 5\n`;
|
|
308
|
+
brand += ` xl: 6.5\n`;
|
|
309
|
+
|
|
310
|
+
writeFileSync(join(targetDir, `${brandId}.brand`), brand, 'utf-8');
|
|
311
|
+
console.log(` \u2713 ${brandId}.brand`);
|
|
312
|
+
|
|
313
|
+
// ── Write a starter .purpose ──
|
|
314
|
+
|
|
315
|
+
let purpose = `id: starter\n`;
|
|
316
|
+
purpose += `name: Starter Purpose\n`;
|
|
317
|
+
purpose += `density: light\n`;
|
|
318
|
+
purpose += `palette: dynamic\n\n`;
|
|
319
|
+
purpose += `slot primary\n`;
|
|
320
|
+
purpose += ` label: Headline\n`;
|
|
321
|
+
purpose += ` typography: headline\n`;
|
|
322
|
+
purpose += ` color: text-primary\n`;
|
|
323
|
+
purpose += ` maxLength: 60\n`;
|
|
324
|
+
purpose += ` samples:\n`;
|
|
325
|
+
purpose += ` - Your headline here\n\n`;
|
|
326
|
+
purpose += `slot secondary\n`;
|
|
327
|
+
purpose += ` label: Subline\n`;
|
|
328
|
+
purpose += ` typography: body\n`;
|
|
329
|
+
purpose += ` color: text-secondary\n`;
|
|
330
|
+
purpose += ` maxLength: 120\n`;
|
|
331
|
+
purpose += ` samples:\n`;
|
|
332
|
+
purpose += ` - Supporting text goes here\n`;
|
|
333
|
+
|
|
334
|
+
const purposesDir = join(targetDir, 'purposes');
|
|
335
|
+
mkdirSync(purposesDir, { recursive: true });
|
|
336
|
+
writeFileSync(join(purposesDir, 'starter.purpose'), purpose, 'utf-8');
|
|
337
|
+
console.log(` \u2713 purposes/starter.purpose`);
|
|
338
|
+
|
|
339
|
+
console.log(`
|
|
340
|
+
Your brand is scaffolded. Next steps:
|
|
341
|
+
- Edit ${brandId}.identity \u2014 add narratives, values, and audiences
|
|
342
|
+
- Edit ${brandId}.brand \u2014 refine colors, add themes, typography styles
|
|
343
|
+
- Replace purposes/starter.purpose with real content purposes
|
|
344
|
+
- Add formats/ and compositions/ when you\u2019re ready for layout`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function collectList() {
|
|
348
|
+
const items = [];
|
|
349
|
+
while (true) {
|
|
350
|
+
const line = await ask(' - ');
|
|
351
|
+
if (!line.trim()) break;
|
|
352
|
+
items.push(line.trim());
|
|
353
|
+
}
|
|
354
|
+
return items;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function collectAudiences() {
|
|
358
|
+
const audiences = [];
|
|
359
|
+
while (true) {
|
|
360
|
+
const id = await input('Audience ID (e.g. "regulars", empty to stop)');
|
|
361
|
+
if (!id) break;
|
|
362
|
+
const label = await input(` Label for "${id}"`);
|
|
363
|
+
const profile = await input(' Profile (demographics, behavior)');
|
|
364
|
+
const motivation = await input(' Motivation');
|
|
365
|
+
const language = await input(' Language style');
|
|
366
|
+
audiences.push({ id, label: label || id, profile, motivation, language });
|
|
367
|
+
console.log('');
|
|
368
|
+
}
|
|
369
|
+
return audiences;
|
|
100
370
|
}
|
|
101
371
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
372
|
+
// ── Interactive init flow ──
|
|
373
|
+
|
|
374
|
+
async function interactiveInit() {
|
|
375
|
+
console.log(' Brand Intent \u2014 What a brand means. Authored, structured, shared.\n');
|
|
376
|
+
|
|
377
|
+
const folder = await input('Project folder');
|
|
378
|
+
if (!folder) {
|
|
379
|
+
console.error('\n A project folder is required.\n');
|
|
108
380
|
process.exit(1);
|
|
109
381
|
}
|
|
110
382
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
383
|
+
const targetDir = resolve(process.cwd(), folder);
|
|
384
|
+
|
|
385
|
+
const mode = await choose('How do you want to start?', [
|
|
386
|
+
{ label: 'Explore the example', value: 'explore' },
|
|
387
|
+
{ label: 'Create your own brand', value: 'create' },
|
|
388
|
+
{ label: 'Just install the AI skill', value: 'skill-only' },
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
// Create project directory
|
|
392
|
+
mkdirSync(targetDir, { recursive: true });
|
|
393
|
+
console.log('');
|
|
394
|
+
|
|
395
|
+
if (mode === 'explore') {
|
|
396
|
+
scaffoldExample(targetDir);
|
|
397
|
+
} else if (mode === 'create') {
|
|
398
|
+
await scaffoldOwnBrand(targetDir);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Always install skills — they're the infrastructure
|
|
402
|
+
console.log('');
|
|
403
|
+
await installSkills(targetDir);
|
|
404
|
+
|
|
405
|
+
closeRl();
|
|
406
|
+
console.log('\n Done. Happy branding.\n');
|
|
115
407
|
}
|
|
116
408
|
|
|
117
409
|
// ── CLI entry point ──
|
|
118
410
|
|
|
119
411
|
const args = process.argv.slice(2);
|
|
120
412
|
const command = args[0];
|
|
121
|
-
const cwd = process.cwd();
|
|
122
413
|
|
|
123
414
|
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8'));
|
|
124
415
|
console.log(`Brand Intent v${pkg.version}\n`);
|
|
125
416
|
|
|
126
417
|
switch (command) {
|
|
127
418
|
case 'install-skills':
|
|
128
|
-
installSkills(cwd);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
case 'init': {
|
|
132
|
-
const fromIdx = args.indexOf('--from');
|
|
133
|
-
if (fromIdx === -1 || !args[fromIdx + 1]) {
|
|
134
|
-
console.error('Usage: brand-intent init --from <brand-name>');
|
|
135
|
-
console.error('Example: brand-intent init --from krume');
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
initFromBrand(args[fromIdx + 1], cwd);
|
|
419
|
+
await installSkills(process.cwd());
|
|
420
|
+
closeRl();
|
|
139
421
|
break;
|
|
140
|
-
}
|
|
141
422
|
|
|
142
|
-
case '
|
|
143
|
-
|
|
144
|
-
console.error('Usage: brand-intent add <brand-name>/brand');
|
|
145
|
-
console.error('Example: brand-intent add krume/brand');
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
148
|
-
addBrand(args[1], cwd);
|
|
423
|
+
case 'init':
|
|
424
|
+
await interactiveInit();
|
|
149
425
|
break;
|
|
150
|
-
}
|
|
151
426
|
|
|
152
427
|
default:
|
|
428
|
+
if (command && command !== '--help' && command !== '-h') {
|
|
429
|
+
console.log(` Unknown command: ${command}\n`);
|
|
430
|
+
}
|
|
153
431
|
console.log('Usage:');
|
|
154
|
-
console.log(' brand-intent
|
|
155
|
-
console.log(' brand-intent
|
|
156
|
-
console.log(' brand-intent add <brand>/brand Add a brand to an existing project');
|
|
432
|
+
console.log(' brand-intent init Guided setup \u2014 explore the example or create your own brand');
|
|
433
|
+
console.log(' brand-intent install-skills Install Brand Intent skill for your AI harness');
|
|
157
434
|
console.log('');
|
|
158
|
-
console.log('
|
|
159
|
-
console.log(' npx brand-intent
|
|
160
|
-
console.log(' npx brand-intent init --from krume');
|
|
161
|
-
console.log(' npx brand-intent add krume/brand');
|
|
435
|
+
console.log('Example:');
|
|
436
|
+
console.log(' npx brand-intent init');
|
|
162
437
|
break;
|
|
163
438
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brand-intent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "An open file format specification for making brand intent machine-readable.",
|
|
5
5
|
"author": "Christophe Stoll <christophe@stoll.studio>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"brand-identity",
|
|
21
21
|
"design-system"
|
|
22
22
|
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"release": "./scripts/release.sh"
|
|
25
|
+
},
|
|
23
26
|
"files": [
|
|
24
27
|
"cli/",
|
|
25
28
|
"skills/",
|