@clafoutis/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1369 @@
1
+ #!/usr/bin/env node
2
+ import * as p2 from '@clack/prompts';
3
+ import { Command } from 'commander';
4
+ import { logger } from '@clafoutis/shared';
5
+ import path2 from 'path';
6
+ import StyleDictionary from 'style-dictionary';
7
+ import { pathToFileURL } from 'url';
8
+ import { Ajv } from 'ajv';
9
+ import fs from 'fs/promises';
10
+ import { spawn } from 'child_process';
11
+
12
+ // src/utils/errors.ts
13
+ var colors = {
14
+ red: (s) => `\x1B[31m${s}\x1B[0m`,
15
+ cyan: (s) => `\x1B[36m${s}\x1B[0m`,
16
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`
17
+ };
18
+ var ClafoutisError = class extends Error {
19
+ constructor(title, detail, suggestion) {
20
+ super(`${title}: ${detail}`);
21
+ this.title = title;
22
+ this.detail = detail;
23
+ this.suggestion = suggestion;
24
+ this.name = "ClafoutisError";
25
+ }
26
+ /**
27
+ * Formats the error for display in the terminal with colors and structure.
28
+ */
29
+ format() {
30
+ let output = `
31
+ ${colors.red("Error:")} ${this.title}
32
+ `;
33
+ output += `
34
+ ${this.detail}
35
+ `;
36
+ if (this.suggestion) {
37
+ output += `
38
+ ${colors.cyan("Suggestion:")} ${this.suggestion}
39
+ `;
40
+ }
41
+ return output;
42
+ }
43
+ };
44
+ function configNotFoundError(configPath, isConsumer) {
45
+ return new ClafoutisError(
46
+ "Configuration not found",
47
+ `Could not find ${configPath}`,
48
+ `Run: npx clafoutis init --${isConsumer ? "consumer" : "producer"}`
49
+ );
50
+ }
51
+ function releaseNotFoundError(version, repo) {
52
+ return new ClafoutisError(
53
+ "Release not found",
54
+ `Version ${version} does not exist in ${repo}`,
55
+ `Check available releases: gh release list -R ${repo}`
56
+ );
57
+ }
58
+ function authRequiredError() {
59
+ return new ClafoutisError(
60
+ "Authentication required",
61
+ "CLAFOUTIS_REPO_TOKEN is required for private repositories",
62
+ "Set the environment variable: export CLAFOUTIS_REPO_TOKEN=ghp_xxx"
63
+ );
64
+ }
65
+ function generatorNotFoundError(name) {
66
+ return new ClafoutisError(
67
+ "Generator not found",
68
+ `Built-in generator "${name}" does not exist`,
69
+ "Available generators: tailwind, figma"
70
+ );
71
+ }
72
+ function pluginLoadError(pluginPath, errorMessage) {
73
+ return new ClafoutisError(
74
+ "Plugin load failed",
75
+ `Could not load generator from ${pluginPath}: ${errorMessage}`,
76
+ 'Ensure the file exports a "generate" function'
77
+ );
78
+ }
79
+ function tokensDirNotFoundError(tokensDir) {
80
+ return new ClafoutisError(
81
+ "Tokens directory not found",
82
+ `Directory "${tokensDir}" does not exist`,
83
+ "Create the directory and add token JSON files"
84
+ );
85
+ }
86
+
87
+ // src/cli/validation.ts
88
+ function validateRepo(value) {
89
+ if (!value) {
90
+ return "Repository is required";
91
+ }
92
+ if (!/^[\w-]+\/[\w.-]+$/.test(value)) {
93
+ return "Repository must be in format: org/repo-name";
94
+ }
95
+ return void 0;
96
+ }
97
+ function validatePath(value) {
98
+ if (!value) {
99
+ return "Path is required";
100
+ }
101
+ if (!value.startsWith("./") && !value.startsWith("/") && value !== ".") {
102
+ return 'Path must start with ./ or /, or be "."';
103
+ }
104
+ return void 0;
105
+ }
106
+ var DEPRECATED_FIELDS = {
107
+ "generators.css": 'Use "generators.tailwind" instead',
108
+ buildDir: 'Renamed to "output"'
109
+ };
110
+ function validateConfig(config, schema, configPath) {
111
+ const ajv = new Ajv({ allErrors: true, strict: false });
112
+ const validate = ajv.compile(schema);
113
+ if (!validate(config)) {
114
+ const errors = validate.errors?.map((e) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n");
115
+ throw new ClafoutisError(
116
+ "Invalid configuration",
117
+ `${configPath}:
118
+ ${errors}`,
119
+ 'Check the config against the schema or run "clafoutis init --force" to regenerate'
120
+ );
121
+ }
122
+ const schemaObj = schema;
123
+ if (schemaObj.properties) {
124
+ const unknownFields = findUnknownFields(config, schemaObj.properties);
125
+ if (unknownFields.length > 0) {
126
+ p2.log.warn(
127
+ `Unknown fields in ${configPath}: ${unknownFields.join(", ")}`
128
+ );
129
+ p2.log.info(
130
+ "These fields will be ignored. Check for typos or outdated config."
131
+ );
132
+ }
133
+ }
134
+ for (const [field, message] of Object.entries(DEPRECATED_FIELDS)) {
135
+ if (hasField(config, field)) {
136
+ p2.log.warn(`Deprecated field "${field}": ${message}`);
137
+ }
138
+ }
139
+ }
140
+ function findUnknownFields(config, schemaProperties, prefix = "") {
141
+ const unknown = [];
142
+ for (const key of Object.keys(config)) {
143
+ const fullPath = prefix ? `${prefix}.${key}` : key;
144
+ if (!(key in schemaProperties)) {
145
+ unknown.push(fullPath);
146
+ } else {
147
+ const value = config[key];
148
+ const schemaProp = schemaProperties[key];
149
+ if (value && typeof value === "object" && !Array.isArray(value) && schemaProp?.properties) {
150
+ unknown.push(
151
+ ...findUnknownFields(
152
+ value,
153
+ schemaProp.properties,
154
+ fullPath
155
+ )
156
+ );
157
+ }
158
+ }
159
+ }
160
+ return unknown;
161
+ }
162
+ function hasField(config, path4) {
163
+ const parts = path4.split(".");
164
+ let current = config;
165
+ for (const part of parts) {
166
+ if (current && typeof current === "object" && part in current) {
167
+ current = current[part];
168
+ } else {
169
+ return false;
170
+ }
171
+ }
172
+ return true;
173
+ }
174
+ function validateProducerFlags(options) {
175
+ const errors = [];
176
+ if (options.tokens) {
177
+ const tokenError = validatePath(options.tokens);
178
+ if (tokenError) {
179
+ errors.push(`--tokens: ${tokenError}`);
180
+ }
181
+ }
182
+ if (options.output) {
183
+ const outputError = validatePath(options.output);
184
+ if (outputError) {
185
+ errors.push(`--output: ${outputError}`);
186
+ }
187
+ }
188
+ if (options.generators) {
189
+ const builtInGenerators = ["tailwind", "figma"];
190
+ const generators = options.generators.split(",").map((g) => g.trim());
191
+ for (const gen of generators) {
192
+ const colonIdx = gen.indexOf(":");
193
+ if (colonIdx > 0) {
194
+ const name = gen.slice(0, colonIdx).trim();
195
+ const pluginPath = gen.slice(colonIdx + 1).trim();
196
+ if (!name) {
197
+ errors.push(
198
+ `--generators: Custom generator "${gen}" has an empty name`
199
+ );
200
+ }
201
+ if (!pluginPath) {
202
+ errors.push(
203
+ `--generators: Custom generator "${name}" is missing a path`
204
+ );
205
+ }
206
+ } else if (!builtInGenerators.includes(gen)) {
207
+ errors.push(
208
+ `--generators: Invalid generator "${gen}". Built-in options: ${builtInGenerators.join(", ")}. For custom generators use "name:./path/to/plugin.js"`
209
+ );
210
+ }
211
+ }
212
+ }
213
+ return errors;
214
+ }
215
+ function validateConsumerFlags(options) {
216
+ const errors = [];
217
+ if (options.repo) {
218
+ const repoError = validateRepo(options.repo);
219
+ if (repoError) {
220
+ errors.push(`--repo: ${repoError}`);
221
+ }
222
+ }
223
+ if (options.files) {
224
+ const mappings = options.files.split(",");
225
+ for (const mapping of mappings) {
226
+ if (!mapping.includes(":")) {
227
+ errors.push(
228
+ `--files: Invalid format "${mapping}". Use format: asset:destination`
229
+ );
230
+ }
231
+ }
232
+ }
233
+ return errors;
234
+ }
235
+
236
+ // src/cli/wizard.ts
237
+ function showIntro(dryRun = false) {
238
+ const suffix = dryRun ? " (DRY RUN)" : "";
239
+ p2.intro(`Clafoutis - GitOps Design Token Generator${suffix}`);
240
+ }
241
+ function showOutro(message) {
242
+ p2.outro(message);
243
+ }
244
+ async function selectMode() {
245
+ const mode = await p2.select({
246
+ message: "What would you like to set up?",
247
+ options: [
248
+ {
249
+ value: "producer",
250
+ label: "Producer",
251
+ hint: "I maintain a design system"
252
+ },
253
+ {
254
+ value: "consumer",
255
+ label: "Consumer",
256
+ hint: "I consume tokens from a design system"
257
+ }
258
+ ]
259
+ });
260
+ if (p2.isCancel(mode)) {
261
+ return null;
262
+ }
263
+ return mode;
264
+ }
265
+ async function runProducerWizard() {
266
+ const answers = await p2.group(
267
+ {
268
+ generators: () => p2.multiselect({
269
+ message: "Which generators would you like to enable?",
270
+ options: [
271
+ { value: "tailwind", label: "Tailwind CSS", hint: "recommended" },
272
+ { value: "figma", label: "Figma Variables" }
273
+ ],
274
+ required: true,
275
+ initialValues: ["tailwind"]
276
+ }),
277
+ tokens: () => p2.text({
278
+ message: "Where are your design tokens located?",
279
+ placeholder: "./tokens",
280
+ initialValue: "./tokens",
281
+ validate: validatePath
282
+ }),
283
+ output: () => p2.text({
284
+ message: "Where should generated files be output?",
285
+ placeholder: "./build",
286
+ initialValue: "./build",
287
+ validate: validatePath
288
+ }),
289
+ workflow: () => p2.confirm({
290
+ message: "Create GitHub Actions workflow for auto-releases?",
291
+ initialValue: true
292
+ })
293
+ },
294
+ {
295
+ onCancel: () => {
296
+ p2.cancel("Setup cancelled.");
297
+ process.exit(0);
298
+ }
299
+ }
300
+ );
301
+ return answers;
302
+ }
303
+ async function runConsumerWizard() {
304
+ const repo = await p2.text({
305
+ message: "GitHub repository (org/repo):",
306
+ placeholder: "Acme/design-system",
307
+ validate: validateRepo
308
+ });
309
+ if (p2.isCancel(repo)) {
310
+ p2.cancel("Setup cancelled.");
311
+ process.exit(0);
312
+ }
313
+ const filesInput = await p2.text({
314
+ message: "Which files do you want to sync? (comma-separated)",
315
+ placeholder: "tailwind.base.css, tailwind.config.js",
316
+ initialValue: "tailwind.base.css, tailwind.config.js"
317
+ });
318
+ if (p2.isCancel(filesInput)) {
319
+ p2.cancel("Setup cancelled.");
320
+ process.exit(0);
321
+ }
322
+ const fileNames = filesInput.split(",").map((f) => f.trim()).filter(Boolean);
323
+ const files = {};
324
+ for (const fileName of fileNames) {
325
+ const defaultDest = suggestDestination(fileName);
326
+ const dest = await p2.text({
327
+ message: `Where should ${fileName} be saved?`,
328
+ placeholder: defaultDest,
329
+ initialValue: defaultDest,
330
+ validate: validatePath
331
+ });
332
+ if (p2.isCancel(dest)) {
333
+ p2.cancel("Setup cancelled.");
334
+ process.exit(0);
335
+ }
336
+ files[fileName] = dest;
337
+ }
338
+ return {
339
+ repo,
340
+ files
341
+ };
342
+ }
343
+ function suggestDestination(fileName) {
344
+ if (fileName.includes("tailwind.config")) {
345
+ return "./tailwind.config.js";
346
+ }
347
+ if (fileName.includes(".css")) {
348
+ return `./src/styles/${fileName}`;
349
+ }
350
+ if (fileName.includes(".scss")) {
351
+ return `./src/styles/${fileName}`;
352
+ }
353
+ return `./${fileName}`;
354
+ }
355
+ async function offerWizard(configType) {
356
+ p2.log.error(`Configuration not found: .clafoutis/${configType}.json`);
357
+ const runWizard = await p2.confirm({
358
+ message: "Would you like to create one now?",
359
+ initialValue: true
360
+ });
361
+ if (p2.isCancel(runWizard)) {
362
+ p2.cancel("Operation cancelled.");
363
+ process.exit(0);
364
+ }
365
+ return runWizard;
366
+ }
367
+ var log3 = {
368
+ info: (message) => p2.log.info(message),
369
+ success: (message) => p2.log.success(message),
370
+ warn: (message) => p2.log.warn(message),
371
+ error: (message) => p2.log.error(message),
372
+ step: (message) => p2.log.step(message),
373
+ message: (message) => p2.log.message(message)
374
+ };
375
+ async function readConfig(configPath) {
376
+ try {
377
+ const content = await fs.readFile(configPath, "utf-8");
378
+ return JSON.parse(content);
379
+ } catch {
380
+ return null;
381
+ }
382
+ }
383
+ async function readProducerConfig(configPath) {
384
+ try {
385
+ const content = await fs.readFile(configPath, "utf-8");
386
+ return JSON.parse(content);
387
+ } catch {
388
+ return null;
389
+ }
390
+ }
391
+ async function fileExists(filePath) {
392
+ try {
393
+ await fs.access(filePath);
394
+ return true;
395
+ } catch {
396
+ return false;
397
+ }
398
+ }
399
+
400
+ // schemas/consumer-config.json
401
+ var consumer_config_default = {
402
+ $schema: "http://json-schema.org/draft-07/schema#",
403
+ type: "object",
404
+ required: ["repo", "version", "files"],
405
+ properties: {
406
+ repo: {
407
+ type: "string",
408
+ pattern: "^[\\w.-]+/[\\w.-]+$",
409
+ description: "GitHub repository in org/name format"
410
+ },
411
+ version: {
412
+ type: "string",
413
+ pattern: "^(latest|v\\d+\\.\\d+\\.\\d+)$",
414
+ description: "Release tag (e.g., v1.0.0) or 'latest'"
415
+ },
416
+ files: {
417
+ type: "object",
418
+ additionalProperties: { type: "string" },
419
+ minProperties: 1,
420
+ description: "Mapping of release asset names to local file paths"
421
+ },
422
+ postSync: {
423
+ type: "string",
424
+ description: "Optional command to run after sync"
425
+ }
426
+ },
427
+ additionalProperties: false
428
+ };
429
+
430
+ // schemas/producer-config.json
431
+ var producer_config_default = {
432
+ $schema: "http://json-schema.org/draft-07/schema#",
433
+ type: "object",
434
+ required: ["tokens", "output", "generators"],
435
+ properties: {
436
+ tokens: {
437
+ type: "string",
438
+ description: "Path to tokens directory"
439
+ },
440
+ output: {
441
+ type: "string",
442
+ description: "Output directory for generated files"
443
+ },
444
+ generators: {
445
+ type: "object",
446
+ additionalProperties: {
447
+ oneOf: [
448
+ { type: "boolean" },
449
+ { type: "string" }
450
+ ]
451
+ },
452
+ description: "Generators to run (true for built-in, string path for custom)"
453
+ }
454
+ },
455
+ additionalProperties: false
456
+ };
457
+
458
+ // src/utils/validate.ts
459
+ function validateConsumerConfig(config) {
460
+ validateConfig(
461
+ config,
462
+ consumer_config_default,
463
+ ".clafoutis/consumer.json"
464
+ );
465
+ }
466
+ function validateProducerConfig(config) {
467
+ validateConfig(
468
+ config,
469
+ producer_config_default,
470
+ ".clafoutis/producer.json"
471
+ );
472
+ }
473
+
474
+ // src/templates/tokens.ts
475
+ var colorPrimitives = {
476
+ color: {
477
+ primary: {
478
+ 50: { $value: "#eff6ff" },
479
+ 100: { $value: "#dbeafe" },
480
+ 200: { $value: "#bfdbfe" },
481
+ 300: { $value: "#93c5fd" },
482
+ 400: { $value: "#60a5fa" },
483
+ 500: { $value: "#3b82f6" },
484
+ 600: { $value: "#2563eb" },
485
+ 700: { $value: "#1d4ed8" },
486
+ 800: { $value: "#1e40af" },
487
+ 900: { $value: "#1e3a8a" }
488
+ },
489
+ neutral: {
490
+ 50: { $value: "#fafafa" },
491
+ 100: { $value: "#f5f5f5" },
492
+ 200: { $value: "#e5e5e5" },
493
+ 300: { $value: "#d4d4d4" },
494
+ 400: { $value: "#a3a3a3" },
495
+ 500: { $value: "#737373" },
496
+ 600: { $value: "#525252" },
497
+ 700: { $value: "#404040" },
498
+ 800: { $value: "#262626" },
499
+ 900: { $value: "#171717" }
500
+ },
501
+ success: {
502
+ 500: { $value: "#22c55e" }
503
+ },
504
+ warning: {
505
+ 500: { $value: "#f59e0b" }
506
+ },
507
+ error: {
508
+ 500: { $value: "#ef4444" }
509
+ }
510
+ }
511
+ };
512
+ var colorDarkPrimitives = {
513
+ color: {
514
+ // Background colors - use lighter neutrals in dark mode
515
+ background: {
516
+ default: { $value: "{color.neutral.900}" },
517
+ subtle: { $value: "{color.neutral.800}" },
518
+ muted: { $value: "{color.neutral.700}" }
519
+ },
520
+ // Foreground/text colors - use darker neutrals (which are lighter) in dark mode
521
+ foreground: {
522
+ default: { $value: "{color.neutral.50}" },
523
+ muted: { $value: "{color.neutral.400}" },
524
+ subtle: { $value: "{color.neutral.500}" }
525
+ },
526
+ // Border colors
527
+ border: {
528
+ default: { $value: "{color.neutral.700}" },
529
+ muted: { $value: "{color.neutral.800}" }
530
+ }
531
+ }
532
+ };
533
+ var spacingPrimitives = {
534
+ spacing: {
535
+ 0: { $value: "0" },
536
+ 1: { $value: "0.25rem" },
537
+ 2: { $value: "0.5rem" },
538
+ 3: { $value: "0.75rem" },
539
+ 4: { $value: "1rem" },
540
+ 5: { $value: "1.25rem" },
541
+ 6: { $value: "1.5rem" },
542
+ 8: { $value: "2rem" },
543
+ 10: { $value: "2.5rem" },
544
+ 12: { $value: "3rem" },
545
+ 16: { $value: "4rem" },
546
+ 20: { $value: "5rem" },
547
+ 24: { $value: "6rem" }
548
+ }
549
+ };
550
+ var typographyPrimitives = {
551
+ fontFamily: {
552
+ sans: { $value: "ui-sans-serif, system-ui, sans-serif" },
553
+ serif: { $value: "ui-serif, Georgia, serif" },
554
+ mono: { $value: "ui-monospace, monospace" }
555
+ },
556
+ fontSize: {
557
+ xs: { $value: "0.75rem" },
558
+ sm: { $value: "0.875rem" },
559
+ base: { $value: "1rem" },
560
+ lg: { $value: "1.125rem" },
561
+ xl: { $value: "1.25rem" },
562
+ "2xl": { $value: "1.5rem" },
563
+ "3xl": { $value: "1.875rem" },
564
+ "4xl": { $value: "2.25rem" }
565
+ },
566
+ fontWeight: {
567
+ normal: { $value: "400" },
568
+ medium: { $value: "500" },
569
+ semibold: { $value: "600" },
570
+ bold: { $value: "700" }
571
+ },
572
+ lineHeight: {
573
+ tight: { $value: "1.25" },
574
+ normal: { $value: "1.5" },
575
+ relaxed: { $value: "1.75" }
576
+ }
577
+ };
578
+ var starterTokens = {
579
+ "colors/primitives.json": colorPrimitives,
580
+ "colors/primitives.dark.json": colorDarkPrimitives,
581
+ "spacing/primitives.json": spacingPrimitives,
582
+ "typography/primitives.json": typographyPrimitives
583
+ };
584
+ function getStarterTokenContent(fileName) {
585
+ const tokens = starterTokens[fileName];
586
+ if (!tokens) {
587
+ throw new Error(`Unknown starter token file: ${fileName}`);
588
+ }
589
+ return JSON.stringify(tokens, null, 2) + "\n";
590
+ }
591
+ function getAllStarterTokens() {
592
+ return Object.keys(starterTokens).map((fileName) => ({
593
+ path: fileName,
594
+ content: getStarterTokenContent(fileName)
595
+ }));
596
+ }
597
+
598
+ // src/templates/workflow.ts
599
+ function getWorkflowTemplate() {
600
+ return `name: Design Token Release
601
+
602
+ on:
603
+ push:
604
+ branches: [main]
605
+ paths:
606
+ - 'tokens/**'
607
+
608
+ jobs:
609
+ release:
610
+ runs-on: ubuntu-latest
611
+ permissions:
612
+ contents: write
613
+
614
+ steps:
615
+ - uses: actions/checkout@v4
616
+ with:
617
+ fetch-depth: 0
618
+
619
+ - uses: actions/setup-node@v4
620
+ with:
621
+ node-version: '22'
622
+
623
+ - name: Install Clafoutis
624
+ run: npm install -D clafoutis
625
+
626
+ - name: Generate tokens
627
+ run: npx clafoutis generate
628
+
629
+ - name: Get next version
630
+ id: version
631
+ run: |
632
+ LATEST=$(git tag -l 'v*' | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+$' | sort -V | tail -n1)
633
+ if [ -z "$LATEST" ]; then
634
+ echo "version=1.0.0" >> $GITHUB_OUTPUT
635
+ else
636
+ VERSION=\${LATEST#v}
637
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
638
+ PATCH=$((PATCH + 1))
639
+ echo "version=\${MAJOR}.\${MINOR}.\${PATCH}" >> $GITHUB_OUTPUT
640
+ fi
641
+
642
+ - name: Prepare release assets
643
+ run: |
644
+ mkdir -p release-assets
645
+ while IFS= read -r -d '' file; do
646
+ relative="\${file#build/}"
647
+ flat_name="\${relative//\\//.}"
648
+ target="release-assets/$flat_name"
649
+ if [ -e "$target" ]; then
650
+ echo "::error::Collision detected: '$relative' flattens to '$flat_name' which already exists"
651
+ exit 1
652
+ fi
653
+ cp "$file" "$target"
654
+ done < <(find build -type f -print0)
655
+
656
+ - name: Create Release
657
+ uses: softprops/action-gh-release@v2
658
+ with:
659
+ tag_name: v\${{ steps.version.outputs.version }}
660
+ name: Design Tokens v\${{ steps.version.outputs.version }}
661
+ generate_release_notes: true
662
+ files: release-assets/*
663
+ env:
664
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
665
+ `;
666
+ }
667
+ function getWorkflowPath() {
668
+ return ".github/workflows/clafoutis-release.yml";
669
+ }
670
+
671
+ // src/commands/init.ts
672
+ function parseGenerators(generatorsString) {
673
+ const entries = generatorsString.split(",").map((g) => g.trim());
674
+ const invalidEntries = [];
675
+ for (const entry of entries) {
676
+ if (!entry) {
677
+ invalidEntries.push("(empty entry - check for extra commas)");
678
+ continue;
679
+ }
680
+ const colonIdx = entry.indexOf(":");
681
+ if (colonIdx === 0) {
682
+ invalidEntries.push(`"${entry}" (missing generator name before ":")`);
683
+ } else if (colonIdx > 0) {
684
+ const name = entry.slice(0, colonIdx).trim();
685
+ const pluginPath = entry.slice(colonIdx + 1).trim();
686
+ if (!name) {
687
+ invalidEntries.push(`"${entry}" (empty generator name)`);
688
+ }
689
+ if (!pluginPath) {
690
+ invalidEntries.push(`"${entry}" (missing path after ":")`);
691
+ }
692
+ }
693
+ }
694
+ if (invalidEntries.length > 0) {
695
+ throw new ClafoutisError(
696
+ "Invalid generator entries",
697
+ `The following entries are malformed:
698
+ - ${invalidEntries.join("\n - ")}`,
699
+ 'Use format "tailwind,figma" for built-ins or "name:./path/to/plugin.js" for custom generators'
700
+ );
701
+ }
702
+ return entries.filter((e) => e.length > 0);
703
+ }
704
+ async function initCommand(options) {
705
+ if (options.producer && options.consumer) {
706
+ throw new ClafoutisError(
707
+ "Conflicting flags",
708
+ "Cannot specify both --producer and --consumer",
709
+ "Choose one: --producer for design system repos, --consumer for application repos"
710
+ );
711
+ }
712
+ const isInteractive = !options.nonInteractive && process.stdin.isTTY;
713
+ const isDryRun = options.dryRun ?? false;
714
+ if (isInteractive) {
715
+ await runInteractiveInit(options, isDryRun);
716
+ } else {
717
+ await runNonInteractiveInit(options, isDryRun);
718
+ }
719
+ }
720
+ async function runInteractiveInit(options, isDryRun) {
721
+ showIntro(isDryRun);
722
+ let mode;
723
+ if (options.producer) {
724
+ mode = "producer";
725
+ } else if (options.consumer) {
726
+ mode = "consumer";
727
+ } else {
728
+ const selectedMode = await selectMode();
729
+ if (!selectedMode) {
730
+ p2.cancel("Setup cancelled.");
731
+ process.exit(0);
732
+ }
733
+ mode = selectedMode;
734
+ }
735
+ if (mode === "producer") {
736
+ const answers = await runProducerWizard();
737
+ if (!answers) {
738
+ return;
739
+ }
740
+ await createProducerConfig(answers, options.force ?? false, isDryRun);
741
+ } else {
742
+ const answers = await runConsumerWizard();
743
+ if (!answers) {
744
+ return;
745
+ }
746
+ await createConsumerConfig(answers, options.force ?? false, isDryRun);
747
+ }
748
+ if (isDryRun) {
749
+ showOutro("No files were written. Remove --dry-run to apply changes.");
750
+ } else {
751
+ showOutro("Setup complete!");
752
+ }
753
+ }
754
+ async function runNonInteractiveInit(options, isDryRun) {
755
+ if (!options.producer && !options.consumer) {
756
+ throw new ClafoutisError(
757
+ "Mode required",
758
+ "In non-interactive mode, you must specify --producer or --consumer",
759
+ "Add --producer or --consumer flag"
760
+ );
761
+ }
762
+ if (options.producer) {
763
+ const errors = validateProducerFlags(options);
764
+ if (errors.length > 0) {
765
+ throw new ClafoutisError(
766
+ "Invalid flags",
767
+ errors.join("\n"),
768
+ "Fix the invalid flags and try again"
769
+ );
770
+ }
771
+ const answers = {
772
+ generators: options.generators ? parseGenerators(options.generators) : ["tailwind"],
773
+ tokens: options.tokens ?? "./tokens",
774
+ output: options.output ?? "./build",
775
+ workflow: options.workflow ?? true
776
+ };
777
+ await createProducerConfig(answers, options.force ?? false, isDryRun);
778
+ } else {
779
+ const errors = validateConsumerFlags(options);
780
+ if (errors.length > 0) {
781
+ throw new ClafoutisError(
782
+ "Invalid flags",
783
+ errors.join("\n"),
784
+ "Fix the invalid flags and try again"
785
+ );
786
+ }
787
+ if (!options.repo) {
788
+ throw new ClafoutisError(
789
+ "Repository required",
790
+ "In non-interactive mode, --repo is required for consumer setup",
791
+ "Add --repo=org/repo-name flag"
792
+ );
793
+ }
794
+ const files = {};
795
+ if (options.files) {
796
+ for (const mapping of options.files.split(",")) {
797
+ const colonIdx = mapping.indexOf(":");
798
+ if (colonIdx === -1) {
799
+ throw new ClafoutisError(
800
+ "Invalid file mapping",
801
+ `Mapping "${mapping.trim()}" is missing a colon separator`,
802
+ 'Use the format "source:destination" (e.g., "tokens.css:./src/styles/tokens.css")'
803
+ );
804
+ }
805
+ const source = mapping.slice(0, colonIdx).trim();
806
+ const dest = mapping.slice(colonIdx + 1).trim();
807
+ if (!source) {
808
+ throw new ClafoutisError(
809
+ "Invalid file mapping",
810
+ `Mapping "${mapping.trim()}" has an empty source`,
811
+ 'Provide a valid asset name before the colon (e.g., "tokens.css:./path")'
812
+ );
813
+ }
814
+ if (!dest) {
815
+ throw new ClafoutisError(
816
+ "Invalid file mapping",
817
+ `Mapping "${mapping.trim()}" has an empty destination`,
818
+ 'Provide a valid path after the colon (e.g., "tokens.css:./path")'
819
+ );
820
+ }
821
+ files[source] = dest;
822
+ }
823
+ } else {
824
+ files["tailwind.base.css"] = "./src/styles/base.css";
825
+ files["tailwind.config.js"] = "./tailwind.config.js";
826
+ }
827
+ const answers = {
828
+ repo: options.repo,
829
+ files
830
+ };
831
+ await createConsumerConfig(answers, options.force ?? false, isDryRun);
832
+ }
833
+ }
834
+ async function createProducerConfig(answers, force, dryRun) {
835
+ const configPath = ".clafoutis/producer.json";
836
+ if (!force && await fileExists(configPath)) {
837
+ throw new ClafoutisError(
838
+ "Configuration already exists",
839
+ configPath,
840
+ "Use --force to overwrite the existing configuration"
841
+ );
842
+ }
843
+ const generators = {};
844
+ for (const entry of answers.generators) {
845
+ const colonIdx = entry.indexOf(":");
846
+ if (colonIdx > 0) {
847
+ const name = entry.slice(0, colonIdx).trim();
848
+ const pluginPath = entry.slice(colonIdx + 1).trim();
849
+ generators[name] = pluginPath;
850
+ } else {
851
+ generators[entry] = true;
852
+ }
853
+ }
854
+ const config = {
855
+ tokens: answers.tokens,
856
+ output: answers.output,
857
+ generators
858
+ };
859
+ const filesToCreate = [
860
+ {
861
+ path: configPath,
862
+ content: JSON.stringify(config, null, 2) + "\n",
863
+ description: `tokens: "${answers.tokens}", output: "${answers.output}"`
864
+ }
865
+ ];
866
+ const starterTokens2 = getAllStarterTokens();
867
+ for (const token of starterTokens2) {
868
+ const tokenPath = path2.join(answers.tokens, token.path);
869
+ if (!force && await fileExists(tokenPath)) {
870
+ continue;
871
+ }
872
+ filesToCreate.push({
873
+ path: tokenPath,
874
+ content: token.content,
875
+ description: "Starter token template"
876
+ });
877
+ }
878
+ if (answers.workflow) {
879
+ const workflowPath = getWorkflowPath();
880
+ if (force || !await fileExists(workflowPath)) {
881
+ filesToCreate.push({
882
+ path: workflowPath,
883
+ content: getWorkflowTemplate(),
884
+ description: "Auto-release workflow on push to main"
885
+ });
886
+ }
887
+ }
888
+ if (dryRun) {
889
+ showDryRunOutput(filesToCreate);
890
+ } else {
891
+ await writeFiles(filesToCreate);
892
+ showNextSteps("producer", answers);
893
+ }
894
+ }
895
+ async function createConsumerConfig(answers, force, dryRun) {
896
+ const configPath = ".clafoutis/consumer.json";
897
+ if (!force && await fileExists(configPath)) {
898
+ throw new ClafoutisError(
899
+ "Configuration already exists",
900
+ configPath,
901
+ "Use --force to overwrite the existing configuration"
902
+ );
903
+ }
904
+ const config = {
905
+ repo: answers.repo,
906
+ version: "latest",
907
+ files: answers.files
908
+ };
909
+ const filesToCreate = [
910
+ {
911
+ path: configPath,
912
+ content: JSON.stringify(config, null, 2) + "\n",
913
+ description: `repo: "${answers.repo}"`
914
+ }
915
+ ];
916
+ if (dryRun) {
917
+ showDryRunOutput(filesToCreate);
918
+ } else {
919
+ await writeFiles(filesToCreate);
920
+ showNextSteps("consumer", answers);
921
+ }
922
+ }
923
+ function showDryRunOutput(files) {
924
+ log3.message("");
925
+ log3.step("Would create the following files:");
926
+ log3.message("");
927
+ for (const file of files) {
928
+ log3.message(` ${file.path}`);
929
+ if (file.description) {
930
+ log3.message(` \u2514\u2500 ${file.description}`);
931
+ }
932
+ }
933
+ log3.message("");
934
+ }
935
+ async function writeFiles(files) {
936
+ for (const file of files) {
937
+ const dir = path2.dirname(file.path);
938
+ await fs.mkdir(dir, { recursive: true });
939
+ await fs.writeFile(file.path, file.content);
940
+ log3.success(`Created ${file.path}`);
941
+ }
942
+ }
943
+ function showNextSteps(mode, answers) {
944
+ log3.message("");
945
+ log3.step("Next steps:");
946
+ if (mode === "producer") {
947
+ const producerAnswers = answers;
948
+ log3.message(
949
+ ` 1. Edit ${producerAnswers.tokens}/colors/primitives.json with your design tokens`
950
+ );
951
+ log3.message(" 2. Run: npx clafoutis generate");
952
+ log3.message(" 3. Push to GitHub - releases will be created automatically");
953
+ } else {
954
+ log3.message(" 1. Run: npx clafoutis sync");
955
+ log3.message(" 2. Add .clafoutis/cache to .gitignore");
956
+ }
957
+ }
958
+
959
+ // src/commands/generate.ts
960
+ async function loadPlugin(pluginPath) {
961
+ const absolutePath = path2.resolve(process.cwd(), pluginPath);
962
+ if (pluginPath.endsWith(".ts")) {
963
+ const { register } = await import('tsx/esm/api');
964
+ register();
965
+ }
966
+ return import(pathToFileURL(absolutePath).href);
967
+ }
968
+ async function generateCommand(options) {
969
+ const configPath = options.config || ".clafoutis/producer.json";
970
+ let config = await readProducerConfig(configPath);
971
+ if (!config) {
972
+ if (await fileExists(configPath)) {
973
+ throw new ClafoutisError(
974
+ "Invalid configuration",
975
+ `Could not parse ${configPath}`,
976
+ "Ensure the file contains valid JSON"
977
+ );
978
+ }
979
+ if (process.stdin.isTTY) {
980
+ const shouldRunWizard = await offerWizard("producer");
981
+ if (shouldRunWizard) {
982
+ await initCommand({ producer: true });
983
+ config = await readProducerConfig(configPath);
984
+ if (!config) {
985
+ throw configNotFoundError(configPath, false);
986
+ }
987
+ } else {
988
+ throw configNotFoundError(configPath, false);
989
+ }
990
+ } else {
991
+ throw configNotFoundError(configPath, false);
992
+ }
993
+ }
994
+ validateProducerConfig(config);
995
+ if (options.tailwind !== void 0 || options.figma !== void 0) {
996
+ config = {
997
+ ...config,
998
+ generators: {
999
+ ...config.generators || {},
1000
+ ...options.tailwind !== void 0 && { tailwind: options.tailwind },
1001
+ ...options.figma !== void 0 && { figma: options.figma }
1002
+ }
1003
+ };
1004
+ }
1005
+ if (options.output) {
1006
+ config.output = options.output;
1007
+ }
1008
+ const tokensDir = path2.resolve(process.cwd(), config.tokens || "./tokens");
1009
+ const outputDir = path2.resolve(process.cwd(), config.output || "./build");
1010
+ if (!await fileExists(tokensDir)) {
1011
+ throw tokensDirNotFoundError(tokensDir);
1012
+ }
1013
+ const generators = config.generators || { tailwind: true, figma: true };
1014
+ if (options.dryRun) {
1015
+ logger.info("[dry-run] Would read tokens from: " + tokensDir);
1016
+ logger.info("[dry-run] Would write to: " + outputDir);
1017
+ for (const [name, value] of Object.entries(generators)) {
1018
+ if (value !== false) {
1019
+ const type = typeof value === "string" ? "custom" : "built-in";
1020
+ logger.info(`[dry-run] Would run generator: ${name} (${type})`);
1021
+ }
1022
+ }
1023
+ return;
1024
+ }
1025
+ logger.info(`Tokens: ${tokensDir}`);
1026
+ logger.info(`Output: ${outputDir}`);
1027
+ let hadFailure = false;
1028
+ for (const [name, value] of Object.entries(generators)) {
1029
+ if (value === false) continue;
1030
+ logger.info(`Running ${name} generator...`);
1031
+ try {
1032
+ let generatorModule;
1033
+ if (typeof value === "string") {
1034
+ try {
1035
+ generatorModule = await loadPlugin(value);
1036
+ } catch (err) {
1037
+ const errorMessage = err instanceof Error ? err.message : String(err);
1038
+ throw pluginLoadError(value, errorMessage);
1039
+ }
1040
+ if (typeof generatorModule.generate !== "function") {
1041
+ throw pluginLoadError(
1042
+ value,
1043
+ 'Module does not export a "generate" function'
1044
+ );
1045
+ }
1046
+ } else {
1047
+ const builtInGenerators = {
1048
+ tailwind: async () => import('@clafoutis/generators/tailwind'),
1049
+ figma: async () => import('@clafoutis/generators/figma')
1050
+ };
1051
+ if (!builtInGenerators[name]) {
1052
+ throw generatorNotFoundError(name);
1053
+ }
1054
+ try {
1055
+ generatorModule = await builtInGenerators[name]();
1056
+ } catch (err) {
1057
+ const errorMessage = err instanceof Error ? err.message : String(err);
1058
+ throw pluginLoadError(`@clafoutis/generators/${name}`, errorMessage);
1059
+ }
1060
+ }
1061
+ const context = {
1062
+ tokensDir,
1063
+ outputDir: path2.join(outputDir, name),
1064
+ config,
1065
+ StyleDictionary
1066
+ };
1067
+ await generatorModule.generate(context);
1068
+ logger.success(`${name} complete`);
1069
+ } catch (err) {
1070
+ if (err instanceof ClafoutisError) {
1071
+ throw err;
1072
+ }
1073
+ logger.error(`${name} failed: ${err}`);
1074
+ hadFailure = true;
1075
+ }
1076
+ }
1077
+ if (hadFailure) {
1078
+ throw new ClafoutisError(
1079
+ "Generation failed",
1080
+ "One or more generators failed",
1081
+ "Check the error messages above and fix the issues"
1082
+ );
1083
+ }
1084
+ logger.success("Generation complete");
1085
+ }
1086
+ var CACHE_DIR = ".clafoutis";
1087
+ var CACHE_FILE = `${CACHE_DIR}/cache`;
1088
+ async function readCache() {
1089
+ try {
1090
+ return (await fs.readFile(CACHE_FILE, "utf-8")).trim();
1091
+ } catch (err) {
1092
+ if (err instanceof Error && err.code === "ENOENT") {
1093
+ return null;
1094
+ }
1095
+ throw err;
1096
+ }
1097
+ }
1098
+ async function writeCache(version) {
1099
+ await fs.mkdir(CACHE_DIR, { recursive: true });
1100
+ await fs.writeFile(CACHE_FILE, version);
1101
+ }
1102
+ async function downloadRelease(config) {
1103
+ const token = process.env.CLAFOUTIS_REPO_TOKEN;
1104
+ const headers = {
1105
+ Accept: "application/vnd.github.v3+json",
1106
+ "User-Agent": "clafoutis-cli"
1107
+ };
1108
+ if (token) {
1109
+ headers["Authorization"] = `token ${token}`;
1110
+ }
1111
+ const isLatest = config.version === "latest";
1112
+ const releaseUrl = isLatest ? `https://api.github.com/repos/${config.repo}/releases/latest` : `https://api.github.com/repos/${config.repo}/releases/tags/${config.version}`;
1113
+ const releaseRes = await fetch(releaseUrl, { headers });
1114
+ if (!releaseRes.ok) {
1115
+ if (releaseRes.status === 404) {
1116
+ throw releaseNotFoundError(config.version, config.repo);
1117
+ } else if (releaseRes.status === 401 || releaseRes.status === 403) {
1118
+ throw authRequiredError();
1119
+ } else {
1120
+ logger.error(`GitHub API error: ${releaseRes.status}`);
1121
+ process.exit(1);
1122
+ }
1123
+ }
1124
+ const release = await releaseRes.json();
1125
+ const resolvedTag = release.tag_name;
1126
+ if (isLatest) {
1127
+ logger.info(`Resolved "latest" to ${resolvedTag}`);
1128
+ }
1129
+ const files = /* @__PURE__ */ new Map();
1130
+ const missingAssets = [];
1131
+ const failedDownloads = [];
1132
+ for (const assetName of Object.keys(config.files)) {
1133
+ const asset = release.assets.find((a) => a.name === assetName);
1134
+ if (!asset) {
1135
+ missingAssets.push(assetName);
1136
+ continue;
1137
+ }
1138
+ logger.info(`Downloading ${assetName}...`);
1139
+ const downloadHeaders = { ...headers, Accept: "application/octet-stream" };
1140
+ const fileRes = await fetch(asset.url, { headers: downloadHeaders });
1141
+ if (!fileRes.ok) {
1142
+ failedDownloads.push(assetName);
1143
+ continue;
1144
+ }
1145
+ files.set(assetName, await fileRes.text());
1146
+ }
1147
+ const errors = [];
1148
+ if (missingAssets.length > 0) {
1149
+ errors.push(`Assets not found in release: ${missingAssets.join(", ")}`);
1150
+ }
1151
+ if (failedDownloads.length > 0) {
1152
+ errors.push(`Failed to download: ${failedDownloads.join(", ")}`);
1153
+ }
1154
+ if (errors.length > 0) {
1155
+ const availableAssets = release.assets.map((a) => a.name).join(", ");
1156
+ throw new ClafoutisError(
1157
+ "Download failed",
1158
+ errors.join("\n"),
1159
+ `Available assets in ${resolvedTag}: ${availableAssets || "none"}`
1160
+ );
1161
+ }
1162
+ return { files, resolvedTag };
1163
+ }
1164
+
1165
+ // src/commands/sync.ts
1166
+ async function writeOutput(config, files) {
1167
+ for (const [assetName, content] of files) {
1168
+ const configPath = config.files[assetName];
1169
+ if (!configPath) continue;
1170
+ const outputPath = path2.resolve(process.cwd(), configPath);
1171
+ await fs.mkdir(path2.dirname(outputPath), { recursive: true });
1172
+ await fs.writeFile(outputPath, content);
1173
+ logger.success(`Written: ${outputPath}`);
1174
+ }
1175
+ }
1176
+ async function syncCommand(options) {
1177
+ const configPath = options.config || ".clafoutis/consumer.json";
1178
+ let config = await readConfig(configPath);
1179
+ if (!config) {
1180
+ if (await fileExists(configPath)) {
1181
+ throw new ClafoutisError(
1182
+ "Invalid configuration",
1183
+ `Could not parse ${configPath}`,
1184
+ "Ensure the file contains valid JSON"
1185
+ );
1186
+ }
1187
+ if (process.stdin.isTTY) {
1188
+ const shouldRunWizard = await offerWizard("consumer");
1189
+ if (shouldRunWizard) {
1190
+ await initCommand({ consumer: true });
1191
+ config = await readConfig(configPath);
1192
+ if (!config) {
1193
+ throw configNotFoundError(configPath, true);
1194
+ }
1195
+ } else {
1196
+ throw configNotFoundError(configPath, true);
1197
+ }
1198
+ } else {
1199
+ throw configNotFoundError(configPath, true);
1200
+ }
1201
+ }
1202
+ validateConsumerConfig(config);
1203
+ const cachedVersion = await readCache();
1204
+ const isLatest = config.version === "latest";
1205
+ logger.info(`Repo: ${config.repo}`);
1206
+ logger.info(`Pinned: ${config.version}`);
1207
+ logger.info(`Cached: ${cachedVersion || "none"}`);
1208
+ if (options.dryRun) {
1209
+ logger.info(
1210
+ "[dry-run] Would download from: " + config.repo + " " + config.version
1211
+ );
1212
+ for (const [assetName, outputPath] of Object.entries(config.files)) {
1213
+ logger.info(`[dry-run] ${assetName} \u2192 ${outputPath}`);
1214
+ }
1215
+ return;
1216
+ }
1217
+ const resolveOutputPaths = () => Object.values(config.files).map((p5) => path2.resolve(process.cwd(), p5));
1218
+ if (!isLatest && !options.force && config.version === cachedVersion) {
1219
+ const outputPaths = resolveOutputPaths();
1220
+ const existsResults = await Promise.all(
1221
+ outputPaths.map((p5) => fileExists(p5))
1222
+ );
1223
+ const allOutputsExist = existsResults.every((exists) => exists);
1224
+ if (allOutputsExist) {
1225
+ logger.success(`Already at ${config.version} - no sync needed`);
1226
+ return;
1227
+ }
1228
+ }
1229
+ logger.warn(`Syncing ${config.version}...`);
1230
+ const { files, resolvedTag } = await downloadRelease(config);
1231
+ if (isLatest && !options.force && resolvedTag === cachedVersion) {
1232
+ const outputPaths = resolveOutputPaths();
1233
+ const existsResults = await Promise.all(
1234
+ outputPaths.map((p5) => fileExists(p5))
1235
+ );
1236
+ const allOutputsExist = existsResults.every((exists) => exists);
1237
+ if (allOutputsExist) {
1238
+ logger.success(`Already at ${resolvedTag} (latest) - no sync needed`);
1239
+ return;
1240
+ }
1241
+ }
1242
+ await writeOutput(config, files);
1243
+ await writeCache(resolvedTag);
1244
+ logger.success(`Synced to ${resolvedTag}`);
1245
+ if (config.postSync) {
1246
+ await runPostSync(config.postSync);
1247
+ }
1248
+ }
1249
+ async function runPostSync(command) {
1250
+ logger.info(`Running postSync: ${command}`);
1251
+ const isWindows = process.platform === "win32";
1252
+ const shell = isWindows ? "cmd.exe" : "/bin/sh";
1253
+ const shellArgs = isWindows ? ["/c", command] : ["-c", command];
1254
+ return new Promise((resolve, reject) => {
1255
+ const child = spawn(shell, shellArgs, {
1256
+ stdio: ["inherit", "pipe", "pipe"],
1257
+ env: process.env
1258
+ });
1259
+ let stdout = "";
1260
+ let stderr = "";
1261
+ child.stdout?.on("data", (data) => {
1262
+ stdout += data.toString();
1263
+ });
1264
+ child.stderr?.on("data", (data) => {
1265
+ stderr += data.toString();
1266
+ });
1267
+ child.on("error", (err) => {
1268
+ reject(
1269
+ new ClafoutisError(
1270
+ "postSync failed",
1271
+ `Failed to spawn command: ${err.message}`,
1272
+ "Check that the command is valid and executable"
1273
+ )
1274
+ );
1275
+ });
1276
+ child.on("close", (code) => {
1277
+ if (code === 0) {
1278
+ if (stdout.trim()) {
1279
+ logger.info(stdout.trim());
1280
+ }
1281
+ logger.success("postSync completed");
1282
+ resolve();
1283
+ } else {
1284
+ const output = [stdout, stderr].filter(Boolean).join("\n").trim();
1285
+ logger.error(`postSync output:
1286
+ ${output || "(no output)"}`);
1287
+ reject(
1288
+ new ClafoutisError(
1289
+ "postSync failed",
1290
+ `Command exited with code ${code}`,
1291
+ "Review the command output above and fix any issues"
1292
+ )
1293
+ );
1294
+ }
1295
+ });
1296
+ });
1297
+ }
1298
+
1299
+ // src/index.ts
1300
+ var program = new Command();
1301
+ function displayError(err) {
1302
+ if (process.stdin.isTTY) {
1303
+ p2.log.error(`${err.title}: ${err.detail}`);
1304
+ if (err.suggestion) {
1305
+ p2.log.info(`Suggestion: ${err.suggestion}`);
1306
+ }
1307
+ } else {
1308
+ console.error(err.format());
1309
+ }
1310
+ }
1311
+ function withErrorHandling(fn) {
1312
+ return async (options) => {
1313
+ try {
1314
+ await fn(options);
1315
+ } catch (err) {
1316
+ if (err instanceof ClafoutisError) {
1317
+ displayError(err);
1318
+ process.exit(1);
1319
+ }
1320
+ throw err;
1321
+ }
1322
+ };
1323
+ }
1324
+ function handleUnexpectedError(err) {
1325
+ if (err instanceof ClafoutisError) {
1326
+ displayError(err);
1327
+ } else {
1328
+ const message = err instanceof Error ? err.message : String(err);
1329
+ if (process.stdin.isTTY) {
1330
+ p2.log.error(`Unexpected error: ${message}`);
1331
+ p2.log.info(
1332
+ "Please report this issue at: https://github.com/Dessert-Labs/clafoutis/issues"
1333
+ );
1334
+ } else {
1335
+ console.error(`
1336
+ Unexpected error: ${message}
1337
+ `);
1338
+ console.error(
1339
+ "Please report this issue at: https://github.com/Dessert-Labs/clafoutis/issues"
1340
+ );
1341
+ }
1342
+ }
1343
+ process.exit(1);
1344
+ }
1345
+ process.on("uncaughtException", handleUnexpectedError);
1346
+ process.on("unhandledRejection", (reason) => {
1347
+ handleUnexpectedError(reason);
1348
+ });
1349
+ program.name("clafoutis").description("GitOps powered design system - generate and sync design tokens").version("0.1.0");
1350
+ program.command("generate").description("Generate platform outputs from design tokens (for producers)").option(
1351
+ "-c, --config <path>",
1352
+ "Path to config file",
1353
+ ".clafoutis/producer.json"
1354
+ ).option("--tailwind", "Generate Tailwind output").option("--figma", "Generate Figma variables").option("-o, --output <dir>", "Output directory", "./build").option("--dry-run", "Preview changes without writing files").action(withErrorHandling(generateCommand));
1355
+ program.command("sync").description("Sync design tokens from GitHub Release (for consumers)").option("-f, --force", "Force sync even if versions match").option(
1356
+ "-c, --config <path>",
1357
+ "Path to config file",
1358
+ ".clafoutis/consumer.json"
1359
+ ).option("--dry-run", "Preview changes without writing files").action(withErrorHandling(syncCommand));
1360
+ program.command("init").description("Initialize Clafoutis configuration").option("--producer", "Set up as a design token producer").option("--consumer", "Set up as a design token consumer").option("-r, --repo <repo>", "GitHub repo for consumer mode (org/name)").option("-t, --tokens <path>", "Token directory path (default: ./tokens)").option("-o, --output <path>", "Output directory path (default: ./build)").option(
1361
+ "-g, --generators <list>",
1362
+ "Comma-separated generators: tailwind, figma"
1363
+ ).option("--workflow", "Create GitHub Actions workflow (default: true)").option("--no-workflow", "Skip GitHub Actions workflow").option(
1364
+ "--files <mapping>",
1365
+ "File mappings for consumer: asset:dest,asset:dest"
1366
+ ).option("--force", "Overwrite existing configuration").option("--dry-run", "Preview changes without writing files").option("--non-interactive", "Skip prompts, use defaults or flags").action(withErrorHandling(initCommand));
1367
+ program.parse();
1368
+ //# sourceMappingURL=index.js.map
1369
+ //# sourceMappingURL=index.js.map