@clafoutis/cli 1.1.2 → 1.2.1

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 CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import * as p2 from '@clack/prompts';
3
3
  import { Command } from 'commander';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { logger } from '@clafoutis/shared';
4
7
  import { generate } from '@clafoutis/generators/figma';
5
8
  import { generate as generate$1 } from '@clafoutis/generators/tailwind';
6
- import { logger } from '@clafoutis/shared';
7
- import path from 'path';
8
9
  import StyleDictionary from 'style-dictionary';
9
10
  import { register } from 'tsx/esm/api';
10
11
  import { fileURLToPath, pathToFileURL } from 'url';
11
12
  import { Ajv } from 'ajv';
12
- import fs from 'fs/promises';
13
- import fs2 from 'fs';
13
+ import fs2 from 'fs/promises';
14
14
  import { spawn } from 'child_process';
15
15
 
16
16
  // src/utils/errors.ts
@@ -88,7 +88,128 @@ function tokensDirNotFoundError(tokensDir) {
88
88
  );
89
89
  }
90
90
 
91
- // src/cli/validation.ts
91
+ // src/utils/cwd.ts
92
+ function resolveCommandCwd(cwdOverride) {
93
+ const cwd = cwdOverride?.trim() ? cwdOverride : process.cwd();
94
+ return path.resolve(cwd);
95
+ }
96
+ function resolveInCwd(commandCwd, targetPath) {
97
+ return path.isAbsolute(targetPath) ? path.resolve(targetPath) : path.resolve(commandCwd, targetPath);
98
+ }
99
+ function displayPath(commandCwd, absolutePath) {
100
+ const rel = path.relative(commandCwd, absolutePath);
101
+ return rel && !rel.startsWith("..") ? rel : absolutePath;
102
+ }
103
+ function validateCwdOption(cwdOverride) {
104
+ if (cwdOverride !== void 0 && cwdOverride.trim() === "") {
105
+ throw new ClafoutisError(
106
+ "Invalid --cwd value",
107
+ "--cwd cannot be empty",
108
+ "Provide a valid directory path, for example: --cwd ./packages/design-system"
109
+ );
110
+ }
111
+ }
112
+
113
+ // src/commands/format.ts
114
+ function loadTokenFiles(dirPath) {
115
+ const files = [];
116
+ function walk(dir, prefix) {
117
+ if (!fs.existsSync(dir)) {
118
+ return;
119
+ }
120
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
121
+ const fullPath = path.join(dir, entry.name);
122
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
123
+ if (entry.isDirectory()) {
124
+ walk(fullPath, relativePath);
125
+ } else if (entry.name.endsWith(".json")) {
126
+ try {
127
+ const content = fs.readFileSync(fullPath, "utf-8");
128
+ files.push({ relativePath, fullPath, content });
129
+ } catch (err) {
130
+ logger.warn(
131
+ `Failed to read ${relativePath}: ${err instanceof Error ? err.message : String(err)}`
132
+ );
133
+ }
134
+ }
135
+ }
136
+ }
137
+ walk(dirPath, "");
138
+ return files;
139
+ }
140
+ function formatJson(content) {
141
+ const parsed = JSON.parse(content);
142
+ return JSON.stringify(parsed, null, 2) + "\n";
143
+ }
144
+ function formatCommand(options) {
145
+ validateCwdOption(options.cwd);
146
+ const commandCwd = resolveCommandCwd(options.cwd);
147
+ const tokensDir = resolveInCwd(commandCwd, options.tokens || "./tokens");
148
+ if (!fs.existsSync(tokensDir)) {
149
+ throw tokensDirNotFoundError(tokensDir);
150
+ }
151
+ logger.info(`Formatting token files in ${tokensDir}...`);
152
+ const files = loadTokenFiles(tokensDir);
153
+ const fileCount = files.length;
154
+ if (fileCount === 0) {
155
+ logger.warn(`No JSON files found in ${tokensDir}`);
156
+ return;
157
+ }
158
+ let changedCount = 0;
159
+ const unformattedFiles = [];
160
+ for (const { relativePath, fullPath, content } of files) {
161
+ try {
162
+ const formatted = formatJson(content);
163
+ if (content !== formatted) {
164
+ if (options.check) {
165
+ unformattedFiles.push(relativePath);
166
+ changedCount++;
167
+ } else if (options.dryRun) {
168
+ logger.info(`Would format: ${relativePath}`);
169
+ changedCount++;
170
+ } else {
171
+ fs.writeFileSync(fullPath, formatted, "utf-8");
172
+ logger.info(`Formatted: ${relativePath}`);
173
+ changedCount++;
174
+ }
175
+ }
176
+ } catch (err) {
177
+ logger.error(
178
+ `Failed to format ${relativePath}: ${err instanceof Error ? err.message : String(err)}`
179
+ );
180
+ throw err;
181
+ }
182
+ }
183
+ if (options.check) {
184
+ if (changedCount > 0) {
185
+ logger.error(
186
+ `${changedCount} of ${fileCount} files are not formatted correctly:`
187
+ );
188
+ for (const file of unformattedFiles) {
189
+ logger.error(` - ${file}`);
190
+ }
191
+ logger.info(
192
+ `Run 'npx clafoutis format --tokens ${tokensDir}' to fix formatting.`
193
+ );
194
+ process.exit(1);
195
+ }
196
+ logger.success(`All ${fileCount} files are correctly formatted`);
197
+ return;
198
+ }
199
+ if (options.dryRun) {
200
+ if (changedCount > 0) {
201
+ logger.info(`Would format ${changedCount} of ${fileCount} files`);
202
+ } else {
203
+ logger.info(`All ${fileCount} files are already formatted`);
204
+ }
205
+ return;
206
+ }
207
+ if (changedCount > 0) {
208
+ logger.success(`Formatted ${changedCount} of ${fileCount} files`);
209
+ } else {
210
+ logger.success(`All ${fileCount} files are already formatted`);
211
+ }
212
+ }
92
213
  function validateRepo(value) {
93
214
  if (!value) {
94
215
  return "Repository is required";
@@ -163,8 +284,8 @@ function findUnknownFields(config, schemaProperties, prefix = "") {
163
284
  }
164
285
  return unknown;
165
286
  }
166
- function hasField(config, path5) {
167
- const parts = path5.split(".");
287
+ function hasField(config, path8) {
288
+ const parts = path8.split(".");
168
289
  let current = config;
169
290
  for (const part of parts) {
170
291
  if (current && typeof current === "object" && part in current) {
@@ -378,7 +499,7 @@ var log3 = {
378
499
  };
379
500
  async function readConfig(configPath) {
380
501
  try {
381
- const content = await fs.readFile(configPath, "utf-8");
502
+ const content = await fs2.readFile(configPath, "utf-8");
382
503
  return JSON.parse(content);
383
504
  } catch {
384
505
  return null;
@@ -386,7 +507,7 @@ async function readConfig(configPath) {
386
507
  }
387
508
  async function readProducerConfig(configPath) {
388
509
  try {
389
- const content = await fs.readFile(configPath, "utf-8");
510
+ const content = await fs2.readFile(configPath, "utf-8");
390
511
  return JSON.parse(content);
391
512
  } catch {
392
513
  return null;
@@ -394,7 +515,7 @@ async function readProducerConfig(configPath) {
394
515
  }
395
516
  async function fileExists(filePath) {
396
517
  try {
397
- await fs.access(filePath);
518
+ await fs2.access(filePath);
398
519
  return true;
399
520
  } catch {
400
521
  return false;
@@ -496,9 +617,9 @@ var __filename$1 = fileURLToPath(import.meta.url);
496
617
  var __dirname$1 = path.dirname(__filename$1);
497
618
  function getTokensDir() {
498
619
  const devPath = path.resolve(__dirname$1, "tokens");
499
- if (fs2.existsSync(devPath)) return devPath;
620
+ if (fs.existsSync(devPath)) return devPath;
500
621
  const distPath = path.resolve(__dirname$1, "templates", "tokens");
501
- if (fs2.existsSync(distPath)) return distPath;
622
+ if (fs.existsSync(distPath)) return distPath;
502
623
  throw new Error(
503
624
  `Starter token templates not found. Searched:
504
625
  ${devPath}
@@ -507,7 +628,7 @@ function getTokensDir() {
507
628
  }
508
629
  function walkTokensDir(dir, base = "") {
509
630
  const result = [];
510
- for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
631
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
511
632
  const relPath = base ? path.join(base, entry.name) : entry.name;
512
633
  const fullPath = path.join(dir, entry.name);
513
634
  if (entry.isDirectory()) {
@@ -515,7 +636,7 @@ function walkTokensDir(dir, base = "") {
515
636
  } else if (entry.name.endsWith(".json")) {
516
637
  result.push({
517
638
  path: relPath,
518
- content: fs2.readFileSync(fullPath, "utf-8")
639
+ content: fs.readFileSync(fullPath, "utf-8")
519
640
  });
520
641
  }
521
642
  }
@@ -535,6 +656,7 @@ on:
535
656
  branches: [main]
536
657
  paths:
537
658
  - 'tokens/**'
659
+ - '.clafoutis/producer.json'
538
660
 
539
661
  jobs:
540
662
  release:
@@ -554,9 +676,24 @@ jobs:
554
676
  - name: Install Clafoutis
555
677
  run: npm install -D @clafoutis/cli
556
678
 
679
+ - name: Check token file formatting
680
+ run: npx clafoutis format --tokens tokens --check
681
+
557
682
  - name: Generate tokens
558
683
  run: npx clafoutis generate
559
684
 
685
+ - name: Commit generated build artifacts
686
+ run: |
687
+ if [ -z "$(git status --porcelain build)" ]; then
688
+ echo "No build changes to commit"
689
+ exit 0
690
+ fi
691
+ git config user.name "github-actions[bot]"
692
+ git config user.email "github-actions[bot]@users.noreply.github.com"
693
+ git add build
694
+ git commit -m "chore: update generated build artifacts"
695
+ git push
696
+
560
697
  - name: Get next version
561
698
  id: version
562
699
  run: |
@@ -640,15 +777,17 @@ async function initCommand(options) {
640
777
  "Choose one: --producer for design system repos, --consumer for application repos"
641
778
  );
642
779
  }
780
+ validateCwdOption(options.cwd);
781
+ const commandCwd = resolveCommandCwd(options.cwd);
643
782
  const isInteractive = !options.nonInteractive && process.stdin.isTTY;
644
783
  const isDryRun = options.dryRun ?? false;
645
784
  if (isInteractive) {
646
- await runInteractiveInit(options, isDryRun);
785
+ await runInteractiveInit(options, isDryRun, commandCwd);
647
786
  } else {
648
- await runNonInteractiveInit(options, isDryRun);
787
+ await runNonInteractiveInit(options, isDryRun, commandCwd);
649
788
  }
650
789
  }
651
- async function runInteractiveInit(options, isDryRun) {
790
+ async function runInteractiveInit(options, isDryRun, commandCwd) {
652
791
  showIntro(isDryRun);
653
792
  let mode;
654
793
  if (options.producer) {
@@ -668,13 +807,23 @@ async function runInteractiveInit(options, isDryRun) {
668
807
  if (!answers) {
669
808
  return;
670
809
  }
671
- await createProducerConfig(answers, options.force ?? false, isDryRun);
810
+ await createProducerConfig(
811
+ answers,
812
+ options.force ?? false,
813
+ isDryRun,
814
+ commandCwd
815
+ );
672
816
  } else {
673
817
  const answers = await runConsumerWizard();
674
818
  if (!answers) {
675
819
  return;
676
820
  }
677
- await createConsumerConfig(answers, options.force ?? false, isDryRun);
821
+ await createConsumerConfig(
822
+ answers,
823
+ options.force ?? false,
824
+ isDryRun,
825
+ commandCwd
826
+ );
678
827
  }
679
828
  if (isDryRun) {
680
829
  showOutro("No files were written. Remove --dry-run to apply changes.");
@@ -682,7 +831,7 @@ async function runInteractiveInit(options, isDryRun) {
682
831
  showOutro("Setup complete!");
683
832
  }
684
833
  }
685
- async function runNonInteractiveInit(options, isDryRun) {
834
+ async function runNonInteractiveInit(options, isDryRun, commandCwd) {
686
835
  if (!options.producer && !options.consumer) {
687
836
  throw new ClafoutisError(
688
837
  "Mode required",
@@ -705,7 +854,12 @@ async function runNonInteractiveInit(options, isDryRun) {
705
854
  output: options.output ?? "./build",
706
855
  workflow: options.workflow ?? true
707
856
  };
708
- await createProducerConfig(answers, options.force ?? false, isDryRun);
857
+ await createProducerConfig(
858
+ answers,
859
+ options.force ?? false,
860
+ isDryRun,
861
+ commandCwd
862
+ );
709
863
  } else {
710
864
  const errors = validateConsumerFlags(options);
711
865
  if (errors.length > 0) {
@@ -759,11 +913,16 @@ async function runNonInteractiveInit(options, isDryRun) {
759
913
  repo: options.repo,
760
914
  files
761
915
  };
762
- await createConsumerConfig(answers, options.force ?? false, isDryRun);
916
+ await createConsumerConfig(
917
+ answers,
918
+ options.force ?? false,
919
+ isDryRun,
920
+ commandCwd
921
+ );
763
922
  }
764
923
  }
765
- async function createProducerConfig(answers, force, dryRun) {
766
- const configPath = ".clafoutis/producer.json";
924
+ async function createProducerConfig(answers, force, dryRun, commandCwd) {
925
+ const configPath = resolveInCwd(commandCwd, ".clafoutis/producer.json");
767
926
  if (!force && await fileExists(configPath)) {
768
927
  throw new ClafoutisError(
769
928
  "Configuration already exists",
@@ -796,7 +955,10 @@ async function createProducerConfig(answers, force, dryRun) {
796
955
  ];
797
956
  const starterTokens = getAllStarterTokens();
798
957
  for (const token of starterTokens) {
799
- const tokenPath = path.join(answers.tokens, token.path);
958
+ const tokenPath = resolveInCwd(
959
+ commandCwd,
960
+ path.join(answers.tokens, token.path)
961
+ );
800
962
  if (!force && await fileExists(tokenPath)) {
801
963
  continue;
802
964
  }
@@ -807,7 +969,7 @@ async function createProducerConfig(answers, force, dryRun) {
807
969
  });
808
970
  }
809
971
  if (answers.workflow) {
810
- const workflowPath = getWorkflowPath();
972
+ const workflowPath = resolveInCwd(commandCwd, getWorkflowPath());
811
973
  if (force || !await fileExists(workflowPath)) {
812
974
  filesToCreate.push({
813
975
  path: workflowPath,
@@ -816,22 +978,23 @@ async function createProducerConfig(answers, force, dryRun) {
816
978
  });
817
979
  }
818
980
  }
819
- if (force || !await fileExists(".gitignore")) {
981
+ const gitignorePath = resolveInCwd(commandCwd, ".gitignore");
982
+ if (force || !await fileExists(gitignorePath)) {
820
983
  filesToCreate.push({
821
- path: ".gitignore",
984
+ path: gitignorePath,
822
985
  content: getProducerGitignore(),
823
986
  description: "Ignore build artifacts and release-assets"
824
987
  });
825
988
  }
826
989
  if (dryRun) {
827
- showDryRunOutput(filesToCreate);
990
+ showDryRunOutput(filesToCreate, commandCwd);
828
991
  } else {
829
- await writeFiles(filesToCreate);
992
+ await writeFiles(filesToCreate, commandCwd);
830
993
  showNextSteps("producer", answers);
831
994
  }
832
995
  }
833
- async function createConsumerConfig(answers, force, dryRun) {
834
- const configPath = ".clafoutis/consumer.json";
996
+ async function createConsumerConfig(answers, force, dryRun, commandCwd) {
997
+ const configPath = resolveInCwd(commandCwd, ".clafoutis/consumer.json");
835
998
  if (!force && await fileExists(configPath)) {
836
999
  throw new ClafoutisError(
837
1000
  "Configuration already exists",
@@ -851,10 +1014,10 @@ async function createConsumerConfig(answers, force, dryRun) {
851
1014
  description: `repo: "${answers.repo}"`
852
1015
  }
853
1016
  ];
854
- const gitignorePath = ".gitignore";
1017
+ const gitignorePath = resolveInCwd(commandCwd, ".gitignore");
855
1018
  const consumerIgnore = getConsumerGitignore();
856
1019
  if (await fileExists(gitignorePath)) {
857
- const existingContent = await fs.readFile(gitignorePath, "utf-8");
1020
+ const existingContent = await fs2.readFile(gitignorePath, "utf-8");
858
1021
  if (!existingContent.includes(".clafoutis/cache")) {
859
1022
  filesToCreate.push({
860
1023
  path: gitignorePath,
@@ -870,30 +1033,30 @@ async function createConsumerConfig(answers, force, dryRun) {
870
1033
  });
871
1034
  }
872
1035
  if (dryRun) {
873
- showDryRunOutput(filesToCreate);
1036
+ showDryRunOutput(filesToCreate, commandCwd);
874
1037
  } else {
875
- await writeFiles(filesToCreate);
1038
+ await writeFiles(filesToCreate, commandCwd);
876
1039
  showNextSteps("consumer", answers);
877
1040
  }
878
1041
  }
879
- function showDryRunOutput(files) {
1042
+ function showDryRunOutput(files, commandCwd) {
880
1043
  log3.message("");
881
1044
  log3.step("Would create the following files:");
882
1045
  log3.message("");
883
1046
  for (const file of files) {
884
- log3.message(` ${file.path}`);
1047
+ log3.message(` ${displayPath(commandCwd, file.path)}`);
885
1048
  if (file.description) {
886
1049
  log3.message(` \u2514\u2500 ${file.description}`);
887
1050
  }
888
1051
  }
889
1052
  log3.message("");
890
1053
  }
891
- async function writeFiles(files) {
1054
+ async function writeFiles(files, commandCwd) {
892
1055
  for (const file of files) {
893
1056
  const dir = path.dirname(file.path);
894
- await fs.mkdir(dir, { recursive: true });
895
- await fs.writeFile(file.path, file.content);
896
- log3.success(`Created ${file.path}`);
1057
+ await fs2.mkdir(dir, { recursive: true });
1058
+ await fs2.writeFile(file.path, file.content);
1059
+ log3.success(`Created ${displayPath(commandCwd, file.path)}`);
897
1060
  }
898
1061
  }
899
1062
  function showNextSteps(mode, answers) {
@@ -904,23 +1067,34 @@ function showNextSteps(mode, answers) {
904
1067
  log3.message(
905
1068
  ` 1. Edit ${producerAnswers.tokens}/colors/primitives.json with your design tokens`
906
1069
  );
907
- log3.message(" 2. Run: npx clafoutis generate");
908
- log3.message(" 3. Push to GitHub - releases will be created automatically");
1070
+ if (producerAnswers.workflow) {
1071
+ log3.message(
1072
+ " 2. Push to GitHub - the workflow will generate outputs and create a release automatically"
1073
+ );
1074
+ } else {
1075
+ log3.message(" 2. Run: npx clafoutis generate");
1076
+ log3.message(" 3. Push to GitHub to share your design system");
1077
+ }
909
1078
  } else {
910
1079
  log3.message(" 1. Run: npx clafoutis sync");
911
1080
  }
912
1081
  }
913
1082
 
914
1083
  // src/commands/generate.ts
915
- async function loadPlugin(pluginPath) {
916
- const absolutePath = path.resolve(process.cwd(), pluginPath);
1084
+ async function loadPlugin(pluginPath, commandCwd) {
1085
+ const absolutePath = resolveInCwd(commandCwd, pluginPath);
917
1086
  if (pluginPath.endsWith(".ts")) {
918
1087
  register();
919
1088
  }
920
1089
  return import(pathToFileURL(absolutePath).href);
921
1090
  }
922
1091
  async function generateCommand(options) {
923
- const configPath = options.config || ".clafoutis/producer.json";
1092
+ validateCwdOption(options.cwd);
1093
+ const commandCwd = resolveCommandCwd(options.cwd);
1094
+ const configPath = resolveInCwd(
1095
+ commandCwd,
1096
+ options.config || ".clafoutis/producer.json"
1097
+ );
924
1098
  let config = await readProducerConfig(configPath);
925
1099
  if (!config) {
926
1100
  if (await fileExists(configPath)) {
@@ -933,7 +1107,7 @@ async function generateCommand(options) {
933
1107
  if (process.stdin.isTTY) {
934
1108
  const shouldRunWizard = await offerWizard("producer");
935
1109
  if (shouldRunWizard) {
936
- await initCommand({ producer: true });
1110
+ await initCommand({ producer: true, cwd: commandCwd });
937
1111
  config = await readProducerConfig(configPath);
938
1112
  if (!config) {
939
1113
  throw configNotFoundError(configPath, false);
@@ -959,8 +1133,8 @@ async function generateCommand(options) {
959
1133
  if (options.output) {
960
1134
  config.output = options.output;
961
1135
  }
962
- const tokensDir = path.resolve(process.cwd(), config.tokens || "./tokens");
963
- const outputDir = path.resolve(process.cwd(), config.output || "./build");
1136
+ const tokensDir = resolveInCwd(commandCwd, config.tokens || "./tokens");
1137
+ const outputDir = resolveInCwd(commandCwd, config.output || "./build");
964
1138
  if (!await fileExists(tokensDir)) {
965
1139
  throw tokensDirNotFoundError(tokensDir);
966
1140
  }
@@ -986,7 +1160,7 @@ async function generateCommand(options) {
986
1160
  let generatorModule;
987
1161
  if (typeof value === "string") {
988
1162
  try {
989
- generatorModule = await loadPlugin(value);
1163
+ generatorModule = await loadPlugin(value, commandCwd);
990
1164
  } catch (err) {
991
1165
  const errorMessage = err instanceof Error ? err.message : String(err);
992
1166
  throw pluginLoadError(value, errorMessage);
@@ -1033,10 +1207,18 @@ async function generateCommand(options) {
1033
1207
  logger.success("Generation complete");
1034
1208
  }
1035
1209
  var CACHE_DIR = ".clafoutis";
1036
- var CACHE_FILE = `${CACHE_DIR}/cache`;
1037
- async function readCache() {
1210
+ var CACHE_FILE = "cache";
1211
+ function getCachePaths(commandCwd) {
1212
+ const dir = path.resolve(commandCwd, CACHE_DIR);
1213
+ return {
1214
+ dir,
1215
+ file: path.join(dir, CACHE_FILE)
1216
+ };
1217
+ }
1218
+ async function readCache(commandCwd = process.cwd()) {
1219
+ const { file } = getCachePaths(commandCwd);
1038
1220
  try {
1039
- return (await fs.readFile(CACHE_FILE, "utf-8")).trim();
1221
+ return (await fs2.readFile(file, "utf-8")).trim();
1040
1222
  } catch (err) {
1041
1223
  if (err instanceof Error && err.code === "ENOENT") {
1042
1224
  return null;
@@ -1044,9 +1226,10 @@ async function readCache() {
1044
1226
  throw err;
1045
1227
  }
1046
1228
  }
1047
- async function writeCache(version) {
1048
- await fs.mkdir(CACHE_DIR, { recursive: true });
1049
- await fs.writeFile(CACHE_FILE, version);
1229
+ async function writeCache(version, commandCwd = process.cwd()) {
1230
+ const { dir, file } = getCachePaths(commandCwd);
1231
+ await fs2.mkdir(dir, { recursive: true });
1232
+ await fs2.writeFile(file, version);
1050
1233
  }
1051
1234
  async function downloadRelease(config) {
1052
1235
  const token = process.env.CLAFOUTIS_REPO_TOKEN;
@@ -1112,18 +1295,23 @@ async function downloadRelease(config) {
1112
1295
  }
1113
1296
 
1114
1297
  // src/commands/sync.ts
1115
- async function writeOutput(config, files) {
1298
+ async function writeOutput(config, files, commandCwd) {
1116
1299
  for (const [assetName, content] of files) {
1117
1300
  const configPath = config.files[assetName];
1118
1301
  if (!configPath) continue;
1119
- const outputPath = path.resolve(process.cwd(), configPath);
1120
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
1121
- await fs.writeFile(outputPath, content);
1302
+ const outputPath = resolveInCwd(commandCwd, configPath);
1303
+ await fs2.mkdir(path.dirname(outputPath), { recursive: true });
1304
+ await fs2.writeFile(outputPath, content);
1122
1305
  logger.success(`Written: ${outputPath}`);
1123
1306
  }
1124
1307
  }
1125
1308
  async function syncCommand(options) {
1126
- const configPath = options.config || ".clafoutis/consumer.json";
1309
+ validateCwdOption(options.cwd);
1310
+ const commandCwd = resolveCommandCwd(options.cwd);
1311
+ const configPath = resolveInCwd(
1312
+ commandCwd,
1313
+ options.config || ".clafoutis/consumer.json"
1314
+ );
1127
1315
  let config = await readConfig(configPath);
1128
1316
  if (!config) {
1129
1317
  if (await fileExists(configPath)) {
@@ -1136,7 +1324,7 @@ async function syncCommand(options) {
1136
1324
  if (process.stdin.isTTY) {
1137
1325
  const shouldRunWizard = await offerWizard("consumer");
1138
1326
  if (shouldRunWizard) {
1139
- await initCommand({ consumer: true });
1327
+ await initCommand({ consumer: true, cwd: commandCwd });
1140
1328
  config = await readConfig(configPath);
1141
1329
  if (!config) {
1142
1330
  throw configNotFoundError(configPath, true);
@@ -1149,7 +1337,7 @@ async function syncCommand(options) {
1149
1337
  }
1150
1338
  }
1151
1339
  validateConsumerConfig(config);
1152
- const cachedVersion = await readCache();
1340
+ const cachedVersion = await readCache(commandCwd);
1153
1341
  const isLatest = config.version === "latest";
1154
1342
  logger.info(`Repo: ${config.repo}`);
1155
1343
  logger.info(`Pinned: ${config.version}`);
@@ -1163,7 +1351,7 @@ async function syncCommand(options) {
1163
1351
  }
1164
1352
  return;
1165
1353
  }
1166
- const resolveOutputPaths = () => Object.values(config.files).map((p5) => path.resolve(process.cwd(), p5));
1354
+ const resolveOutputPaths = () => Object.values(config.files).map((p5) => resolveInCwd(commandCwd, p5));
1167
1355
  if (!isLatest && !options.force && config.version === cachedVersion) {
1168
1356
  const outputPaths = resolveOutputPaths();
1169
1357
  const existsResults = await Promise.all(
@@ -1188,14 +1376,14 @@ async function syncCommand(options) {
1188
1376
  return;
1189
1377
  }
1190
1378
  }
1191
- await writeOutput(config, files);
1192
- await writeCache(resolvedTag);
1379
+ await writeOutput(config, files, commandCwd);
1380
+ await writeCache(resolvedTag, commandCwd);
1193
1381
  logger.success(`Synced to ${resolvedTag}`);
1194
1382
  if (config.postSync) {
1195
- await runPostSync(config.postSync);
1383
+ await runPostSync(config.postSync, commandCwd);
1196
1384
  }
1197
1385
  }
1198
- async function runPostSync(command) {
1386
+ async function runPostSync(command, commandCwd) {
1199
1387
  logger.info(`Running postSync: ${command}`);
1200
1388
  const isWindows = process.platform === "win32";
1201
1389
  const shell = isWindows ? "cmd.exe" : "/bin/sh";
@@ -1203,7 +1391,8 @@ async function runPostSync(command) {
1203
1391
  return new Promise((resolve, reject) => {
1204
1392
  const child = spawn(shell, shellArgs, {
1205
1393
  stdio: ["inherit", "pipe", "pipe"],
1206
- env: process.env
1394
+ env: process.env,
1395
+ cwd: commandCwd
1207
1396
  });
1208
1397
  let stdout = "";
1209
1398
  let stderr = "";
@@ -1300,19 +1489,23 @@ program.command("generate").description("Generate platform outputs from design t
1300
1489
  "-c, --config <path>",
1301
1490
  "Path to config file",
1302
1491
  ".clafoutis/producer.json"
1303
- ).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));
1492
+ ).option("--tailwind", "Generate Tailwind output").option("--figma", "Generate Figma variables").option("-o, --output <dir>", "Output directory", "./build").option("--cwd <path>", "Run command as if from this directory").option("--dry-run", "Preview changes without writing files").action(withErrorHandling(generateCommand));
1304
1493
  program.command("sync").description("Sync design tokens from GitHub Release (for consumers)").option("-f, --force", "Force sync even if versions match").option(
1305
1494
  "-c, --config <path>",
1306
1495
  "Path to config file",
1307
1496
  ".clafoutis/consumer.json"
1308
- ).option("--dry-run", "Preview changes without writing files").action(withErrorHandling(syncCommand));
1497
+ ).option("--cwd <path>", "Run command as if from this directory").option("--dry-run", "Preview changes without writing files").action(withErrorHandling(syncCommand));
1309
1498
  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(
1310
1499
  "-g, --generators <list>",
1311
1500
  "Comma-separated generators: tailwind, figma"
1312
1501
  ).option("--workflow", "Create GitHub Actions workflow (default: true)").option("--no-workflow", "Skip GitHub Actions workflow").option(
1313
1502
  "--files <mapping>",
1314
1503
  "File mappings for consumer: asset:dest,asset:dest"
1315
- ).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));
1504
+ ).option("--force", "Overwrite existing configuration").option("--dry-run", "Preview changes without writing files").option("--non-interactive", "Skip prompts, use defaults or flags").option("--cwd <path>", "Run command as if from this directory").action(withErrorHandling(initCommand));
1505
+ program.command("format").description("Format token JSON files for consistent formatting").option("-t, --tokens <path>", "Token directory path", "./tokens").option(
1506
+ "--check",
1507
+ "Check formatting without modifying files (fails if unformatted)"
1508
+ ).option("--cwd <path>", "Run command as if from this directory").option("--dry-run", "Preview changes without writing files").action(withErrorHandling(formatCommand));
1316
1509
  program.parse();
1317
1510
  //# sourceMappingURL=index.js.map
1318
1511
  //# sourceMappingURL=index.js.map