@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/LICENSE +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1369 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -0
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
|