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 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 stoll.brand-intent-lang
32
+ ext install studiostoll.brand-intent-lang
33
33
  ```
34
34
 
35
- ### 2. Install skills for your AI agent
35
+ ### 2. Run init
36
36
 
37
- ```bash
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 --from krume
40
+ npx brand-intent init
49
41
  ```
50
42
 
51
43
  ## Documentation
@@ -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
- # Libre Franklin: utilitarian, no-nonsense, legible.
49
- # Not a display typeface. Not decorative. Feels like signage.
58
+ # Hanken Grotesk: clean, geometric, contemporary.
59
+ # ABC Stefan Simple: display face for hero moments.
50
60
 
51
61
  font primary
52
- name: Libre Franklin
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 # cqh - large but not shouting
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: 500
66
- size: 3.5
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: 300
87
- size: 14 # intentionally large - display element
88
- lineHeight: 0.9
89
- letterSpacing: -0.03
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: xs
147
+ spacing: s
109
148
 
110
- divider short
111
- thickness: 0.2
112
- width: 25%
149
+ divider ruler
150
+ thickness: 0.8
151
+ width: 20%
113
152
  color: divider
114
153
  align: left
115
- spacing: s
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 HARNESS_DIRS = {
16
- claude: '.claude/commands',
17
- cursor: '.cursor/rules',
18
- gemini: '.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
- const found = [];
24
- for (const [name, dir] of Object.entries(HARNESS_DIRS)) {
25
- // Check if the harness config directory exists (or parent does)
26
- const parent = dir.split('/')[0];
27
- if (existsSync(join(cwd, parent))) {
28
- found.push({ name, dir });
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 found;
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
- // ── Commands ──
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
- const harnesses = detectHarnesses(cwd);
94
+ let harnesses = detectHarnesses(cwd);
44
95
 
45
96
  if (harnesses.length === 0) {
46
- // No harness detected — install to .claude/ by default
47
- harnesses.push({ name: 'claude', dir: HARNESS_DIRS.claude });
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
- let installed = 0;
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(` ✓ ${name} — ${dir}/brand-intent.md`);
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
- function copyDirRecursive(src, dest) {
64
- mkdirSync(dest, { recursive: true });
65
- for (const entry of readdirSync(src)) {
66
- const srcPath = join(src, entry);
67
- const destPath = join(dest, entry);
68
- if (statSync(srcPath).isDirectory()) {
69
- copyDirRecursive(srcPath, destPath);
70
- } else {
71
- copyFileSync(srcPath, destPath);
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
- function initFromBrand(brandName, cwd) {
77
- const brandSrc = join(PKG_ROOT, 'brands', brandName);
78
- if (!existsSync(brandSrc)) {
79
- console.error(`Error: brand "${brandName}" not found.`);
80
- console.error(`Available brands: ${readdirSync(join(PKG_ROOT, 'brands')).join(', ')}`);
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
- // Copy brand files
85
- const brandDest = join(cwd, `brands/${brandName}`);
86
- copyDirRecursive(brandSrc, brandDest);
87
- console.log(` ✓ brand files → brands/${brandName}/`);
88
-
89
- // Copy shared layers (formats, purposes, compositions)
90
- for (const layer of ['formats', 'purposes', 'compositions']) {
91
- const layerSrc = join(PKG_ROOT, layer);
92
- if (existsSync(layerSrc) && readdirSync(layerSrc).length > 0) {
93
- const layerDest = join(cwd, layer);
94
- copyDirRecursive(layerSrc, layerDest);
95
- console.log(` ✓ ${layer}/ → ${layer}/`);
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
- console.log(`\nScaffolded from "${brandName}". Edit the .identity and .brand files to make it yours.`);
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
- function addBrand(brandRef, cwd) {
103
- // brandRef is "brandName/brand" — extract the brand name
104
- const brandName = brandRef.split('/')[0];
105
- const brandSrc = join(PKG_ROOT, 'brands', brandName);
106
- if (!existsSync(brandSrc)) {
107
- console.error(`Error: brand "${brandName}" not found.`);
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 brandDest = join(cwd, `brands/${brandName}`);
112
- copyDirRecursive(brandSrc, brandDest);
113
- console.log(` ✓ brand files → brands/${brandName}/`);
114
- console.log(`\nAdded "${brandName}" brand files to your project.`);
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
- break;
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 'add': {
143
- if (!args[1]) {
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 install-skills Install Brand Intent skill for your AI harness');
155
- console.log(' brand-intent init --from <brand> Scaffold a project from a reference brand');
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('Examples:');
159
- console.log(' npx brand-intent install-skills');
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",
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/",