@josui/token-studio 0.1.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/cli.js ADDED
@@ -0,0 +1,676 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import path7 from "path";
5
+ import process from "process";
6
+ import { Command } from "commander";
7
+ import open from "open";
8
+
9
+ // src/shared/constants.ts
10
+ var SUPPORTED_TYPES = [
11
+ "color",
12
+ "dimension",
13
+ "duration",
14
+ "cubicBezier",
15
+ "fontFamily",
16
+ "number",
17
+ "string"
18
+ ];
19
+ var DEFAULT_TOKENS_RELATIVE_DIR = "packages/tokens/src/tokens";
20
+ var DEFAULT_TERRAZZO_RELATIVE_PATH = "packages/tokens/terrazzo.config.mjs";
21
+
22
+ // src/shared/path-utils.ts
23
+ import path from "path";
24
+ function normalizeCategoryName(name) {
25
+ return name.trim().toLowerCase();
26
+ }
27
+ function categoryFileName(name) {
28
+ return `${name}.json`;
29
+ }
30
+ function validateCategoryName(name) {
31
+ if (!name) {
32
+ return "Category name is required";
33
+ }
34
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
35
+ return "Category name must be kebab-case";
36
+ }
37
+ return null;
38
+ }
39
+ function inferTerrazzoPathFromTokensDir(tokensDir, fallback) {
40
+ const normalized = path.normalize(tokensDir);
41
+ const suffix = path.normalize(path.join("src", "tokens"));
42
+ if (normalized.endsWith(suffix)) {
43
+ return path.join(normalized, "..", "..", "terrazzo.config.mjs");
44
+ }
45
+ return fallback;
46
+ }
47
+
48
+ // src/shared/config.ts
49
+ import fs from "fs/promises";
50
+ import path2 from "path";
51
+ var CONFIG_FILE_NAMES = ["token-studio.config.json", ".token-studio.json"];
52
+ async function exists(filePath) {
53
+ try {
54
+ await fs.access(filePath);
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+ async function findConfigFile(startDir) {
61
+ let currentDir = path2.resolve(startDir);
62
+ while (true) {
63
+ for (const fileName of CONFIG_FILE_NAMES) {
64
+ const candidate = path2.join(currentDir, fileName);
65
+ if (await exists(candidate)) {
66
+ return candidate;
67
+ }
68
+ }
69
+ const parentDir = path2.dirname(currentDir);
70
+ if (parentDir === currentDir) {
71
+ break;
72
+ }
73
+ currentDir = parentDir;
74
+ }
75
+ return null;
76
+ }
77
+ async function readConfig(configPath) {
78
+ const content = await fs.readFile(configPath, "utf8");
79
+ const parsed = JSON.parse(content);
80
+ return parsed;
81
+ }
82
+ async function findNearestTokensRoot(startDir) {
83
+ let currentDir = path2.resolve(startDir);
84
+ while (true) {
85
+ const candidate = path2.join(currentDir, "packages/tokens/src/tokens");
86
+ if (await exists(candidate)) {
87
+ return candidate;
88
+ }
89
+ const parentDir = path2.dirname(currentDir);
90
+ if (parentDir === currentDir) {
91
+ break;
92
+ }
93
+ currentDir = parentDir;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ // src/server/index.ts
99
+ import { serve } from "@hono/node-server";
100
+ import path6 from "path";
101
+
102
+ // src/server/api.ts
103
+ import path3 from "path";
104
+ import fs2 from "fs/promises";
105
+ import { Hono } from "hono";
106
+ import { serveStatic } from "@hono/node-server/serve-static";
107
+
108
+ // src/shared/schemas.ts
109
+ import { z } from "zod";
110
+ var supportedTypes = z.enum(SUPPORTED_TYPES);
111
+ var createCategorySchema = z.object({
112
+ name: z.string().trim().min(1),
113
+ type: supportedTypes
114
+ });
115
+ var saveCategorySchema = z.object({
116
+ document: z.record(z.string(), z.unknown())
117
+ });
118
+ var validateSchema = z.object({
119
+ name: z.string().trim().min(1),
120
+ document: z.record(z.string(), z.unknown())
121
+ });
122
+
123
+ // src/shared/validators.ts
124
+ function isObject(value) {
125
+ return typeof value === "object" && value !== null && !Array.isArray(value);
126
+ }
127
+ function isReferenceValue(value) {
128
+ return typeof value === "string" && /^\{[a-z0-9.-]+(?:\.[a-z0-9.-]+)*\}$/i.test(value);
129
+ }
130
+ function isSupportedType(type) {
131
+ return typeof type === "string" && SUPPORTED_TYPES.includes(type);
132
+ }
133
+ function validateTypedLiteral(type, value) {
134
+ if (isReferenceValue(value)) {
135
+ return null;
136
+ }
137
+ switch (type) {
138
+ case "color": {
139
+ if (!isObject(value)) {
140
+ return "Color value must be an object";
141
+ }
142
+ if (typeof value.colorSpace !== "string") {
143
+ return "Color value needs colorSpace";
144
+ }
145
+ if (!Array.isArray(value.components) || value.components.some((c) => typeof c !== "number")) {
146
+ return "Color value needs numeric components array";
147
+ }
148
+ return null;
149
+ }
150
+ case "dimension":
151
+ case "duration": {
152
+ if (!isObject(value)) {
153
+ return `${type} value must be an object`;
154
+ }
155
+ if (typeof value.value !== "number") {
156
+ return `${type} value needs numeric value`;
157
+ }
158
+ if (typeof value.unit !== "string") {
159
+ return `${type} value needs unit`;
160
+ }
161
+ return null;
162
+ }
163
+ case "cubicBezier": {
164
+ if (!Array.isArray(value) || value.length !== 4 || value.some((n) => typeof n !== "number")) {
165
+ return "cubicBezier value must be an array of four numbers";
166
+ }
167
+ return null;
168
+ }
169
+ case "fontFamily": {
170
+ if (typeof value === "string") {
171
+ return null;
172
+ }
173
+ if (Array.isArray(value) && value.every((part) => typeof part === "string")) {
174
+ return null;
175
+ }
176
+ return "fontFamily value must be string or string[]";
177
+ }
178
+ case "number": {
179
+ if (typeof value !== "number") {
180
+ return "number token must have numeric value";
181
+ }
182
+ return null;
183
+ }
184
+ case "string": {
185
+ if (typeof value !== "string") {
186
+ return "string token must have string value";
187
+ }
188
+ return null;
189
+ }
190
+ default:
191
+ return "Unsupported token type";
192
+ }
193
+ }
194
+ function validateCategoryDocument(name, document) {
195
+ const issues = [];
196
+ const rootKeys = Object.keys(document);
197
+ if (rootKeys.length !== 1) {
198
+ issues.push({ path: "$", message: "Document must contain exactly one root key" });
199
+ return { valid: false, issues };
200
+ }
201
+ const [rootKey] = rootKeys;
202
+ if (rootKey !== name) {
203
+ issues.push({ path: "$", message: `Root key must match category name "${name}"` });
204
+ }
205
+ const rootValue = document[rootKey];
206
+ if (!isObject(rootValue)) {
207
+ issues.push({ path: rootKey, message: "Root value must be an object" });
208
+ return { valid: false, issues };
209
+ }
210
+ function traverse(node, path8, context) {
211
+ const localType = isSupportedType(node.$type) ? node.$type : context.inheritedType;
212
+ const hasValue = Object.prototype.hasOwnProperty.call(node, "$value");
213
+ if (hasValue) {
214
+ if (!localType) {
215
+ issues.push({
216
+ path: path8,
217
+ message: "Token must define $type directly or inherit one from parent"
218
+ });
219
+ } else {
220
+ const typedIssue = validateTypedLiteral(localType, node.$value);
221
+ if (typedIssue) {
222
+ issues.push({ path: `${path8}.$value`, message: typedIssue });
223
+ }
224
+ }
225
+ if (Object.prototype.hasOwnProperty.call(node, "$extensions")) {
226
+ const extensions = node.$extensions;
227
+ if (!isObject(extensions)) {
228
+ issues.push({ path: `${path8}.$extensions`, message: "$extensions must be an object" });
229
+ } else if (Object.prototype.hasOwnProperty.call(extensions, "mode")) {
230
+ const mode = extensions.mode;
231
+ if (!isObject(mode)) {
232
+ issues.push({ path: `${path8}.$extensions.mode`, message: "mode must be an object" });
233
+ } else {
234
+ for (const modeKey of ["light", "dark"]) {
235
+ if (Object.prototype.hasOwnProperty.call(mode, modeKey) && localType) {
236
+ const typedIssue = validateTypedLiteral(localType, mode[modeKey]);
237
+ if (typedIssue) {
238
+ issues.push({
239
+ path: `${path8}.$extensions.mode.${modeKey}`,
240
+ message: typedIssue
241
+ });
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+ for (const [key, value] of Object.entries(node)) {
250
+ if (key.startsWith("$")) {
251
+ continue;
252
+ }
253
+ if (!isObject(value)) {
254
+ issues.push({ path: `${path8}.${key}`, message: "Group/token nodes must be objects" });
255
+ continue;
256
+ }
257
+ traverse(value, `${path8}.${key}`, {
258
+ inheritedType: localType
259
+ });
260
+ }
261
+ }
262
+ traverse(rootValue, rootKey, {
263
+ inheritedType: isSupportedType(rootValue.$type) ? rootValue.$type : void 0
264
+ });
265
+ return {
266
+ valid: issues.length === 0,
267
+ issues
268
+ };
269
+ }
270
+
271
+ // src/server/api.ts
272
+ function createApi(repository, webDir) {
273
+ const app = new Hono();
274
+ app.get("/api/health", (context) => {
275
+ return context.json({
276
+ status: "ok",
277
+ tokensDir: repository.tokensDir,
278
+ terrazzoPath: repository.terrazzoPath
279
+ });
280
+ });
281
+ app.get("/api/categories", async (context) => {
282
+ const categories = await repository.listCategories();
283
+ return context.json({ categories });
284
+ });
285
+ app.get("/api/categories/:name", async (context) => {
286
+ const name = context.req.param("name");
287
+ try {
288
+ const category = await repository.getCategory(name);
289
+ return context.json(category);
290
+ } catch (error) {
291
+ return context.json({ error: error.message }, 404);
292
+ }
293
+ });
294
+ app.post("/api/categories", async (context) => {
295
+ const body = await context.req.json();
296
+ const parsed = createCategorySchema.safeParse(body);
297
+ if (!parsed.success) {
298
+ return context.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, 400);
299
+ }
300
+ try {
301
+ const created = await repository.createCategory(parsed.data.name, parsed.data.type);
302
+ return context.json(created, 201);
303
+ } catch (error) {
304
+ return context.json({ error: error.message }, 400);
305
+ }
306
+ });
307
+ app.put("/api/categories/:name", async (context) => {
308
+ const name = context.req.param("name");
309
+ const body = await context.req.json();
310
+ const parsed = saveCategorySchema.safeParse(body);
311
+ if (!parsed.success) {
312
+ return context.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, 400);
313
+ }
314
+ try {
315
+ const saved = await repository.saveCategory(name, parsed.data.document);
316
+ return context.json(saved);
317
+ } catch (error) {
318
+ return context.json({ error: error.message }, 400);
319
+ }
320
+ });
321
+ app.delete("/api/categories/:name", async (context) => {
322
+ const name = context.req.param("name");
323
+ await repository.deleteCategory(name);
324
+ return context.body(null, 204);
325
+ });
326
+ app.post("/api/validate", async (context) => {
327
+ const body = await context.req.json();
328
+ const parsed = validateSchema.safeParse(body);
329
+ if (!parsed.success) {
330
+ return context.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, 400);
331
+ }
332
+ const result = validateCategoryDocument(parsed.data.name, parsed.data.document);
333
+ return context.json(result);
334
+ });
335
+ app.use("*", serveStatic({ root: webDir }));
336
+ app.get("*", async (context) => {
337
+ const requestPath = context.req.path;
338
+ if (requestPath.startsWith("/api/")) {
339
+ return context.notFound();
340
+ }
341
+ if (path3.extname(requestPath)) {
342
+ return context.notFound();
343
+ }
344
+ const indexPath = path3.join(webDir, "index.html");
345
+ try {
346
+ const html = await fs2.readFile(indexPath, "utf8");
347
+ return context.html(html);
348
+ } catch {
349
+ return context.text(
350
+ "Token Studio web assets are missing. Run `pnpm --filter @josui/token-studio build` first.",
351
+ 503
352
+ );
353
+ }
354
+ });
355
+ return app;
356
+ }
357
+
358
+ // src/server/repository.ts
359
+ import fs5 from "fs/promises";
360
+ import path5 from "path";
361
+
362
+ // src/shared/document.ts
363
+ function isObject2(value) {
364
+ return typeof value === "object" && value !== null && !Array.isArray(value);
365
+ }
366
+ function asSupportedType(value) {
367
+ if (value === "color" || value === "dimension" || value === "duration" || value === "cubicBezier" || value === "fontFamily" || value === "number" || value === "string") {
368
+ return value;
369
+ }
370
+ return void 0;
371
+ }
372
+ function flattenTokens(document) {
373
+ const rootKeys = Object.keys(document);
374
+ if (rootKeys.length !== 1) {
375
+ return [];
376
+ }
377
+ const [rootKey] = rootKeys;
378
+ const root = document[rootKey];
379
+ if (!isObject2(root)) {
380
+ return [];
381
+ }
382
+ const tokens = [];
383
+ function walk(node, currentPath, inheritedType) {
384
+ const localType = asSupportedType(node.$type) ?? inheritedType;
385
+ if (Object.prototype.hasOwnProperty.call(node, "$value") && localType) {
386
+ const entry = {
387
+ path: currentPath,
388
+ type: localType,
389
+ description: typeof node.$description === "string" ? node.$description : void 0,
390
+ value: node.$value,
391
+ hasMode: false
392
+ };
393
+ if (isObject2(node.$extensions) && isObject2(node.$extensions.mode)) {
394
+ entry.hasMode = true;
395
+ entry.mode = {
396
+ light: node.$extensions.mode.light,
397
+ dark: node.$extensions.mode.dark
398
+ };
399
+ }
400
+ tokens.push(entry);
401
+ }
402
+ for (const [key, value] of Object.entries(node)) {
403
+ if (key.startsWith("$")) {
404
+ continue;
405
+ }
406
+ if (!isObject2(value)) {
407
+ continue;
408
+ }
409
+ walk(value, `${currentPath}.${key}`, localType);
410
+ }
411
+ }
412
+ walk(root, rootKey, asSupportedType(root.$type));
413
+ return tokens.sort((a, b) => a.path.localeCompare(b.path));
414
+ }
415
+
416
+ // src/server/fs-utils.ts
417
+ import fs3 from "fs/promises";
418
+ import path4 from "path";
419
+ import { randomUUID } from "crypto";
420
+ async function readJsonFile(filePath) {
421
+ const content = await fs3.readFile(filePath, "utf8");
422
+ return JSON.parse(content);
423
+ }
424
+ async function writeJsonAtomic(filePath, document) {
425
+ const directory = path4.dirname(filePath);
426
+ const tempPath = path4.join(directory, `.${path4.basename(filePath)}.${randomUUID()}.tmp`);
427
+ const payload = `${JSON.stringify(document, null, 2)}
428
+ `;
429
+ await fs3.writeFile(tempPath, payload, "utf8");
430
+ await fs3.rename(tempPath, filePath);
431
+ }
432
+ async function removeFileIfExists(filePath) {
433
+ try {
434
+ await fs3.unlink(filePath);
435
+ } catch (error) {
436
+ if (error.code !== "ENOENT") {
437
+ throw error;
438
+ }
439
+ }
440
+ }
441
+ async function ensureDirectory(directory) {
442
+ await fs3.mkdir(directory, { recursive: true });
443
+ }
444
+
445
+ // src/server/terrazzo.ts
446
+ import fs4 from "fs/promises";
447
+ function parseTokenArray(configContent) {
448
+ const match = /tokens\s*:\s*\[(?<entries>[\s\S]*?)\],/m.exec(configContent);
449
+ if (!match || !match.groups) {
450
+ return null;
451
+ }
452
+ const fullMatch = match[0];
453
+ const start = match.index;
454
+ const end = start + fullMatch.length;
455
+ const entriesBlock = match.groups.entries;
456
+ const entries = Array.from(entriesBlock.matchAll(/'([^']+)'/g), (entry) => entry[1]);
457
+ return {
458
+ entries,
459
+ start,
460
+ end
461
+ };
462
+ }
463
+ function formatTokenArray(entries) {
464
+ const formattedEntries = entries.map((entry) => ` '${entry}',`).join("\n");
465
+ return `tokens: [
466
+ ${formattedEntries}
467
+ ],`;
468
+ }
469
+ async function syncTerrazzoTokens(terrazzoPath, tokenRelativePath, operation) {
470
+ let configContent;
471
+ try {
472
+ configContent = await fs4.readFile(terrazzoPath, "utf8");
473
+ } catch (error) {
474
+ if (error.code === "ENOENT") {
475
+ return;
476
+ }
477
+ throw error;
478
+ }
479
+ const parsed = parseTokenArray(configContent);
480
+ if (!parsed) {
481
+ return;
482
+ }
483
+ let nextEntries = parsed.entries;
484
+ if (operation === "add") {
485
+ if (!nextEntries.includes(tokenRelativePath)) {
486
+ nextEntries = [...nextEntries, tokenRelativePath];
487
+ }
488
+ } else {
489
+ nextEntries = nextEntries.filter((entry) => entry !== tokenRelativePath);
490
+ }
491
+ const replacement = formatTokenArray(nextEntries);
492
+ const nextConfig = `${configContent.slice(0, parsed.start)}${replacement}${configContent.slice(parsed.end)}`;
493
+ if (nextConfig !== configContent) {
494
+ await fs4.writeFile(terrazzoPath, nextConfig, "utf8");
495
+ }
496
+ }
497
+
498
+ // src/server/repository.ts
499
+ var TokenRepository = class {
500
+ constructor(options) {
501
+ this.options = options;
502
+ }
503
+ get tokensDir() {
504
+ return this.options.tokensDir;
505
+ }
506
+ get terrazzoPath() {
507
+ return this.options.terrazzoPath;
508
+ }
509
+ async listCategories() {
510
+ await ensureDirectory(this.tokensDir);
511
+ const entries = await fs5.readdir(this.tokensDir, { withFileTypes: true });
512
+ const categories = [];
513
+ for (const entry of entries) {
514
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
515
+ continue;
516
+ }
517
+ const name = entry.name.replace(/\.json$/, "");
518
+ categories.push({
519
+ name,
520
+ fileName: entry.name,
521
+ path: path5.join(this.tokensDir, entry.name)
522
+ });
523
+ }
524
+ return categories.sort((a, b) => a.name.localeCompare(b.name));
525
+ }
526
+ categoryPath(name) {
527
+ return path5.join(this.tokensDir, categoryFileName(name));
528
+ }
529
+ async getCategory(name) {
530
+ const normalized = normalizeCategoryName(name);
531
+ const filePath = this.categoryPath(normalized);
532
+ const document = await readJsonFile(filePath);
533
+ return {
534
+ name: normalized,
535
+ document,
536
+ tokens: flattenTokens(document)
537
+ };
538
+ }
539
+ async createCategory(name, type) {
540
+ const normalized = normalizeCategoryName(name);
541
+ const nameError = validateCategoryName(normalized);
542
+ if (nameError) {
543
+ throw new Error(nameError);
544
+ }
545
+ const filePath = this.categoryPath(normalized);
546
+ try {
547
+ await fs5.access(filePath);
548
+ throw new Error(`Category ${normalized} already exists`);
549
+ } catch (error) {
550
+ if (error.code !== "ENOENT") {
551
+ throw error;
552
+ }
553
+ }
554
+ const document = {
555
+ [normalized]: {
556
+ $type: type
557
+ }
558
+ };
559
+ const validation = validateCategoryDocument(normalized, document);
560
+ if (!validation.valid) {
561
+ throw new Error(validation.issues[0]?.message ?? "Validation failed");
562
+ }
563
+ await writeJsonAtomic(filePath, document);
564
+ await syncTerrazzoTokens(
565
+ this.terrazzoPath,
566
+ `./src/tokens/${categoryFileName(normalized)}`,
567
+ "add"
568
+ );
569
+ return {
570
+ name: normalized,
571
+ document,
572
+ tokens: []
573
+ };
574
+ }
575
+ async saveCategory(name, document) {
576
+ const normalized = normalizeCategoryName(name);
577
+ const validation = validateCategoryDocument(normalized, document);
578
+ if (!validation.valid) {
579
+ throw new Error(
580
+ validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n")
581
+ );
582
+ }
583
+ const filePath = this.categoryPath(normalized);
584
+ await writeJsonAtomic(filePath, document);
585
+ return {
586
+ name: normalized,
587
+ document,
588
+ tokens: flattenTokens(document)
589
+ };
590
+ }
591
+ async deleteCategory(name) {
592
+ const normalized = normalizeCategoryName(name);
593
+ await removeFileIfExists(this.categoryPath(normalized));
594
+ await syncTerrazzoTokens(
595
+ this.terrazzoPath,
596
+ `./src/tokens/${categoryFileName(normalized)}`,
597
+ "remove"
598
+ );
599
+ }
600
+ };
601
+
602
+ // src/server/index.ts
603
+ function startServer(options) {
604
+ const repository = new TokenRepository({
605
+ tokensDir: options.tokensDir,
606
+ terrazzoPath: options.terrazzoPath
607
+ });
608
+ const app = createApi(repository, path6.resolve(options.webDir));
609
+ const server = serve({
610
+ fetch: app.fetch,
611
+ port: options.port
612
+ });
613
+ return {
614
+ url: `http://127.0.0.1:${options.port}`,
615
+ stop: async () => {
616
+ await new Promise((resolve, reject) => {
617
+ server.close((error) => {
618
+ if (error) {
619
+ reject(error);
620
+ return;
621
+ }
622
+ resolve();
623
+ });
624
+ });
625
+ }
626
+ };
627
+ }
628
+
629
+ // src/cli.ts
630
+ async function run() {
631
+ const program = new Command();
632
+ program.name("josui-token-studio").description("Launch local token CRUD editor").option("--cwd <path>", "Working directory", process.cwd()).option("--config <path>", "Path to token-studio config JSON").option("--tokens-dir <path>", "Tokens directory path").option("--port <number>", "Port to run local server", "4598").option("--no-open", "Do not open browser automatically");
633
+ program.parse(process.argv);
634
+ const options = program.opts();
635
+ const cwd = path7.resolve(options.cwd);
636
+ const explicitConfigPath = options.config ? path7.resolve(cwd, options.config) : null;
637
+ const discoveredConfigPath = explicitConfigPath ?? await findConfigFile(cwd);
638
+ const config = discoveredConfigPath ? await readConfig(discoveredConfigPath) : {};
639
+ const configBaseDir = discoveredConfigPath ? path7.dirname(discoveredConfigPath) : cwd;
640
+ const discoveredTokensDir = await findNearestTokensRoot(cwd);
641
+ const configTokensDir = config.tokensDir ? path7.resolve(configBaseDir, config.tokensDir) : void 0;
642
+ const tokensDir = path7.resolve(
643
+ options.tokensDir ?? configTokensDir ?? discoveredTokensDir ?? path7.join(cwd, DEFAULT_TOKENS_RELATIVE_DIR)
644
+ );
645
+ const defaultTerrazzoPath = path7.resolve(
646
+ config.terrazzoPath ? path7.resolve(configBaseDir, config.terrazzoPath) : path7.join(cwd, DEFAULT_TERRAZZO_RELATIVE_PATH)
647
+ );
648
+ const terrazzoPath = inferTerrazzoPathFromTokensDir(tokensDir, defaultTerrazzoPath);
649
+ const server = startServer({
650
+ tokensDir,
651
+ terrazzoPath,
652
+ port: Number(options.port),
653
+ webDir: path7.resolve(import.meta.dirname, "..", "dist", "web")
654
+ });
655
+ process.stdout.write(`Token Studio running at ${server.url}
656
+ `);
657
+ if (discoveredConfigPath) {
658
+ process.stdout.write(`config: ${discoveredConfigPath}
659
+ `);
660
+ }
661
+ process.stdout.write(`tokensDir: ${tokensDir}
662
+ `);
663
+ process.stdout.write(`terrazzo: ${terrazzoPath}
664
+ `);
665
+ if (options.open) {
666
+ await open(server.url);
667
+ }
668
+ const shutdown = async () => {
669
+ await server.stop();
670
+ process.exit(0);
671
+ };
672
+ process.on("SIGINT", shutdown);
673
+ process.on("SIGTERM", shutdown);
674
+ }
675
+ void run();
676
+ //# sourceMappingURL=cli.js.map