spec-snake 0.0.1-beta.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,783 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { defineCommand as defineCommand3, runMain } from "citty";
5
+
6
+ // src/cli/commands/init.ts
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import { defineCommand } from "citty";
10
+ import { consola } from "consola";
11
+ var CONFIG_TEMPLATE = `// For more detailed configuration examples, see:
12
+ // https://github.com/cut0/spec-snake/blob/main/examples/spec-snake.ts
13
+
14
+ import { defineConfig, defineScenario } from '@cut0/spec-snake';
15
+
16
+ export default defineConfig({
17
+ scenarios: [
18
+ defineScenario({
19
+ id: 'default',
20
+ name: 'Design Doc Generator',
21
+ steps: [
22
+ {
23
+ slug: 'overview',
24
+ title: 'Overview',
25
+ description: 'Basic information about the feature',
26
+ section: {
27
+ type: 'single',
28
+ name: 'overview',
29
+ fields: [
30
+ {
31
+ type: 'input',
32
+ id: 'title',
33
+ label: 'Title',
34
+ description: 'Feature title',
35
+ placeholder: 'Enter feature title',
36
+ required: true,
37
+ },
38
+ {
39
+ type: 'textarea',
40
+ id: 'description',
41
+ label: 'Description',
42
+ description: 'Detailed description of the feature',
43
+ placeholder: 'Describe the feature...',
44
+ rows: 4,
45
+ },
46
+ {
47
+ type: 'select',
48
+ id: 'priority',
49
+ label: 'Priority',
50
+ description: 'Feature priority level',
51
+ placeholder: 'Select priority',
52
+ options: [
53
+ { value: 'high', label: 'High' },
54
+ { value: 'medium', label: 'Medium' },
55
+ { value: 'low', label: 'Low' },
56
+ ],
57
+ },
58
+ ],
59
+ },
60
+ },
61
+ ],
62
+ overrides: {
63
+ filename: (params) => {
64
+ return \`\${params.timestamp}.md\`;
65
+ },
66
+ },
67
+ prompt:
68
+ 'Generate a design doc based on the following input: {{INPUT_JSON}}',
69
+ }),
70
+ ],
71
+ permissions: {
72
+ allowSave: true,
73
+ },
74
+ });
75
+ `;
76
+ var initCommand = defineCommand({
77
+ meta: {
78
+ name: "init",
79
+ description: "Initialize a new spec-snake.config.ts file in the current directory"
80
+ },
81
+ args: {
82
+ output: {
83
+ type: "string",
84
+ description: "Output file path",
85
+ alias: "o",
86
+ default: "spec-snake.config.ts"
87
+ },
88
+ force: {
89
+ type: "boolean",
90
+ description: "Overwrite existing file",
91
+ alias: "f",
92
+ default: false
93
+ }
94
+ },
95
+ async run({ args }) {
96
+ const outputPath = path.resolve(process.cwd(), args.output);
97
+ if (fs.existsSync(outputPath) && !args.force) {
98
+ consola.error(`File already exists: ${outputPath}`);
99
+ consola.info("Use --force (-f) to overwrite");
100
+ process.exit(1);
101
+ }
102
+ try {
103
+ fs.writeFileSync(outputPath, CONFIG_TEMPLATE, "utf-8");
104
+ consola.success(`Config file created: ${outputPath}`);
105
+ } catch (error) {
106
+ consola.error("Failed to create config file:", error);
107
+ process.exit(1);
108
+ }
109
+ }
110
+ });
111
+
112
+ // src/cli/commands/start.ts
113
+ import * as fs2 from "node:fs";
114
+ import * as path2 from "node:path";
115
+ import * as url from "node:url";
116
+ import { serve } from "@hono/node-server";
117
+ import { serveStatic } from "@hono/node-server/serve-static";
118
+ import { defineCommand as defineCommand2 } from "citty";
119
+ import { consola as consola2 } from "consola";
120
+ import { createJiti } from "jiti";
121
+
122
+ // src/schema.ts
123
+ import * as v from "valibot";
124
+ var isLayoutField = (field) => {
125
+ return field.type === "grid";
126
+ };
127
+ var FieldBaseSchema = v.object({
128
+ id: v.string(),
129
+ label: v.string(),
130
+ description: v.string(),
131
+ placeholder: v.optional(v.string()),
132
+ required: v.optional(v.boolean())
133
+ });
134
+ var SelectOptionSchema = v.object({
135
+ value: v.string(),
136
+ label: v.string()
137
+ });
138
+ var InputFieldSchema = v.object({
139
+ ...FieldBaseSchema.entries,
140
+ type: v.literal("input"),
141
+ inputType: v.optional(v.picklist(["text", "date", "url"])),
142
+ suggestions: v.optional(v.array(v.string()))
143
+ });
144
+ var TextareaFieldSchema = v.object({
145
+ ...FieldBaseSchema.entries,
146
+ type: v.literal("textarea"),
147
+ rows: v.optional(v.number())
148
+ });
149
+ var SelectFieldSchema = v.object({
150
+ ...FieldBaseSchema.entries,
151
+ type: v.literal("select"),
152
+ options: v.array(SelectOptionSchema)
153
+ });
154
+ var CheckboxFieldSchema = v.object({
155
+ ...FieldBaseSchema.entries,
156
+ type: v.literal("checkbox")
157
+ });
158
+ var FormFieldSchema = v.union([
159
+ InputFieldSchema,
160
+ TextareaFieldSchema,
161
+ SelectFieldSchema,
162
+ CheckboxFieldSchema
163
+ ]);
164
+ var GridLayoutSchema = v.object({
165
+ type: v.literal("grid"),
166
+ columns: v.number(),
167
+ fields: v.array(v.lazy(() => FieldSchema))
168
+ });
169
+ var FieldSchema = v.union([
170
+ FormFieldSchema,
171
+ GridLayoutSchema
172
+ ]);
173
+ var SingleSectionSchema = v.object({
174
+ type: v.literal("single"),
175
+ name: v.string(),
176
+ fields: v.array(FieldSchema)
177
+ });
178
+ var ArraySectionSchema = v.object({
179
+ type: v.literal("array"),
180
+ name: v.string(),
181
+ fields: v.array(FieldSchema),
182
+ minFieldCount: v.optional(v.number())
183
+ });
184
+ var SectionSchema = v.union([SingleSectionSchema, ArraySectionSchema]);
185
+ var StepSchema = v.object({
186
+ slug: v.string(),
187
+ title: v.string(),
188
+ description: v.string(),
189
+ section: SectionSchema
190
+ });
191
+ var McpServerConfigSchema = v.union([
192
+ v.object({
193
+ type: v.optional(v.literal("stdio")),
194
+ command: v.string(),
195
+ args: v.optional(v.array(v.string())),
196
+ env: v.optional(v.record(v.string(), v.string()))
197
+ }),
198
+ v.object({
199
+ type: v.literal("sse"),
200
+ url: v.string(),
201
+ headers: v.optional(v.record(v.string(), v.string()))
202
+ }),
203
+ v.object({
204
+ type: v.literal("http"),
205
+ url: v.string(),
206
+ headers: v.optional(v.record(v.string(), v.string()))
207
+ })
208
+ ]);
209
+ var AiSettingsSchema = v.optional(
210
+ v.object({
211
+ model: v.optional(v.string()),
212
+ fallbackModel: v.optional(v.string()),
213
+ maxThinkingTokens: v.optional(v.number()),
214
+ maxTurns: v.optional(v.number()),
215
+ maxBudgetUsd: v.optional(v.number()),
216
+ allowedTools: v.optional(v.array(v.string())),
217
+ disallowedTools: v.optional(v.array(v.string())),
218
+ tools: v.optional(
219
+ v.union([
220
+ v.array(v.string()),
221
+ v.object({
222
+ type: v.literal("preset"),
223
+ preset: v.literal("claude_code")
224
+ })
225
+ ])
226
+ ),
227
+ permissionMode: v.optional(
228
+ v.picklist([
229
+ "default",
230
+ "acceptEdits",
231
+ "bypassPermissions",
232
+ "plan",
233
+ "delegate",
234
+ "dontAsk"
235
+ ])
236
+ ),
237
+ allowDangerouslySkipPermissions: v.optional(v.boolean()),
238
+ mcpServers: v.optional(v.record(v.string(), McpServerConfigSchema)),
239
+ strictMcpConfig: v.optional(v.boolean())
240
+ })
241
+ );
242
+ var ScenarioBaseSchema = v.object({
243
+ id: v.string(),
244
+ name: v.string(),
245
+ steps: v.array(StepSchema),
246
+ prompt: v.string(),
247
+ aiSettings: AiSettingsSchema
248
+ });
249
+ var ScenarioSchema = ScenarioBaseSchema;
250
+ var PermissionsSchema = v.object({
251
+ allowSave: v.boolean()
252
+ });
253
+ var ConfigSchema = v.object({
254
+ scenarios: v.array(ScenarioSchema),
255
+ permissions: PermissionsSchema
256
+ });
257
+ var safeParseConfig = (data) => {
258
+ return v.safeParse(ConfigSchema, data);
259
+ };
260
+
261
+ // src/server/api.ts
262
+ import { Hono as Hono3 } from "hono";
263
+
264
+ // src/server/apps/docs.ts
265
+ import { Hono } from "hono";
266
+ import { createMiddleware } from "hono/factory";
267
+
268
+ // src/server/helpers/docs/transform.ts
269
+ var transformFormData = (body, sectionInfoMap) => {
270
+ const items = [];
271
+ for (const [sectionName, sectionValue] of Object.entries(body)) {
272
+ const sectionInfo = sectionInfoMap.get(sectionName);
273
+ if (sectionInfo == null) {
274
+ continue;
275
+ }
276
+ const fieldInfoMap = new Map(sectionInfo.fields.map((f) => [f.id, f]));
277
+ const values = (Array.isArray(sectionValue) ? sectionValue : [sectionValue]).map((item) => {
278
+ const itemValues = Object.entries(item).map(([fieldId, fieldValue]) => {
279
+ const fieldInfo = fieldInfoMap.get(fieldId);
280
+ if (fieldInfo) {
281
+ return {
282
+ label: fieldInfo.label,
283
+ description: fieldInfo.description,
284
+ value: fieldValue
285
+ };
286
+ }
287
+ return null;
288
+ }).filter((value) => value != null);
289
+ return itemValues;
290
+ });
291
+ items.push({
292
+ title: sectionInfo.title,
293
+ description: sectionInfo.description,
294
+ values
295
+ });
296
+ }
297
+ return { items };
298
+ };
299
+
300
+ // src/server/repositories/document.ts
301
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
302
+ import { join } from "node:path";
303
+
304
+ // src/server/helpers/docs/metadata.ts
305
+ var METADATA_START = "<!-- design-docs-metadata";
306
+ var METADATA_END = "-->";
307
+ var serializeMetadata = (metadata) => {
308
+ return `${METADATA_START}
309
+ ${JSON.stringify(metadata, null, 2)}
310
+ ${METADATA_END}`;
311
+ };
312
+ var parseMetadata = (content) => {
313
+ const metadataStartIndex = content.lastIndexOf(METADATA_START);
314
+ if (metadataStartIndex === -1) {
315
+ return { metadata: null, content };
316
+ }
317
+ const metadataEndIndex = content.indexOf(METADATA_END, metadataStartIndex);
318
+ if (metadataEndIndex === -1) {
319
+ return { metadata: null, content };
320
+ }
321
+ try {
322
+ const metadataJson = content.slice(metadataStartIndex + METADATA_START.length, metadataEndIndex).trim();
323
+ const metadata = JSON.parse(metadataJson);
324
+ const cleanContent = content.slice(0, metadataStartIndex).trim();
325
+ return { metadata, content: cleanContent };
326
+ } catch {
327
+ return { metadata: null, content };
328
+ }
329
+ };
330
+ var addMetadataToContent = (content, metadata) => {
331
+ return `${content}
332
+
333
+ ${serializeMetadata(metadata)}`;
334
+ };
335
+
336
+ // src/server/repositories/document.ts
337
+ var getOutputDir = (scenario) => {
338
+ return scenario.outputDir ?? join(process.cwd(), "output");
339
+ };
340
+ var readDocument = async (scenario, filename) => {
341
+ const outputDir = getOutputDir(scenario);
342
+ const filePath = join(outputDir, filename);
343
+ try {
344
+ const rawContent = await readFile(filePath, "utf-8");
345
+ const { metadata, content } = parseMetadata(rawContent);
346
+ if (metadata?.scenarioId !== scenario.id) {
347
+ return { success: false, error: "scenario_mismatch" };
348
+ }
349
+ return {
350
+ success: true,
351
+ doc: { filename, content, metadata }
352
+ };
353
+ } catch {
354
+ return { success: false, error: "not_found" };
355
+ }
356
+ };
357
+ var getDocumentsForScenario = async (scenario) => {
358
+ const outputDir = getOutputDir(scenario);
359
+ try {
360
+ const files = await readdir(outputDir);
361
+ const mdFiles = files.filter((file) => file.endsWith(".md"));
362
+ const docs = await Promise.all(
363
+ mdFiles.map(async (filename) => {
364
+ const result = await readDocument(scenario, filename);
365
+ return result.success ? result.doc : null;
366
+ })
367
+ );
368
+ return docs.filter((doc) => doc != null);
369
+ } catch {
370
+ return [];
371
+ }
372
+ };
373
+ var saveDocument = async ({
374
+ scenario,
375
+ scenarioId,
376
+ filename,
377
+ content,
378
+ formData
379
+ }) => {
380
+ const outputDir = getOutputDir(scenario);
381
+ const outputPath = join(outputDir, filename);
382
+ const contentWithMetadata = addMetadataToContent(content, {
383
+ scenarioId,
384
+ formData
385
+ });
386
+ await mkdir(outputDir, { recursive: true });
387
+ await writeFile(outputPath, contentWithMetadata, "utf-8");
388
+ return { outputPath };
389
+ };
390
+ var getFilename = (scenario, scenarioId, content, formData, inputData) => {
391
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
392
+ const filenameOverride = scenario.overrides?.filename;
393
+ if (filenameOverride != null) {
394
+ return typeof filenameOverride === "function" ? filenameOverride({
395
+ scenarioId,
396
+ timestamp,
397
+ content,
398
+ formData,
399
+ inputData
400
+ }) : filenameOverride;
401
+ }
402
+ return `design-doc-${scenarioId}-${timestamp}.md`;
403
+ };
404
+
405
+ // src/server/usecases/docs/generate-doc.ts
406
+ import {
407
+ query
408
+ } from "@anthropic-ai/claude-agent-sdk";
409
+ var generateDesignDoc = async ({
410
+ scenario,
411
+ formData,
412
+ inputData
413
+ }) => {
414
+ const promptTemplate = typeof scenario.prompt === "function" ? scenario.prompt({ formData, inputData }) : scenario.prompt;
415
+ const prompt = promptTemplate.replace(
416
+ "{{INPUT_JSON}}",
417
+ JSON.stringify(inputData, null, 2)
418
+ );
419
+ let message = null;
420
+ for await (const msg of query({
421
+ prompt,
422
+ options: scenario.aiSettings
423
+ })) {
424
+ if (msg.type === "result" && msg.subtype === "success") {
425
+ message = msg;
426
+ }
427
+ }
428
+ if (message == null) {
429
+ throw new Error("Query failed");
430
+ }
431
+ return message.result;
432
+ };
433
+
434
+ // src/server/apps/docs.ts
435
+ var createScenarioMiddleware = (scenarioInfoMap) => createMiddleware(async (c, next) => {
436
+ const scenarioId = c.req.param("scenarioId");
437
+ if (scenarioId == null) {
438
+ return c.json({ error: "Scenario ID is required" }, 400);
439
+ }
440
+ const scenarioInfo = scenarioInfoMap.get(scenarioId);
441
+ if (scenarioInfo == null) {
442
+ return c.json({ error: "Scenario not found" }, 404);
443
+ }
444
+ c.set("scenarioInfo", scenarioInfo);
445
+ await next();
446
+ });
447
+ var createDocsApp = (config, scenarioInfoMap) => {
448
+ const app = new Hono();
449
+ app.use(
450
+ "/api/scenarios/:scenarioId/*",
451
+ createScenarioMiddleware(scenarioInfoMap)
452
+ );
453
+ app.get("/api/scenarios/:scenarioId/docs", async (c) => {
454
+ const { scenario } = c.get("scenarioInfo");
455
+ const docs = await getDocumentsForScenario(scenario);
456
+ return c.json({ docs });
457
+ });
458
+ app.post("/api/scenarios/:scenarioId/docs/preview", async (c) => {
459
+ const { scenario, sectionInfoMap } = c.get("scenarioInfo");
460
+ const formData = await c.req.json();
461
+ const inputData = transformFormData(formData, sectionInfoMap);
462
+ const content = await generateDesignDoc({ scenario, formData, inputData });
463
+ if (scenario.hooks?.onPreview != null) {
464
+ await scenario.hooks.onPreview({ formData, inputData, content });
465
+ }
466
+ return c.json({ success: true, content });
467
+ });
468
+ app.post("/api/scenarios/:scenarioId/docs", async (c) => {
469
+ const scenarioId = c.req.param("scenarioId");
470
+ const { scenario, sectionInfoMap } = c.get("scenarioInfo");
471
+ if (!config.permissions.allowSave) {
472
+ return c.json({ error: "Save is not allowed" }, 403);
473
+ }
474
+ const { content, formData } = await c.req.json();
475
+ const inputData = transformFormData(formData, sectionInfoMap);
476
+ const filename = getFilename(
477
+ scenario,
478
+ scenarioId,
479
+ content,
480
+ formData,
481
+ inputData
482
+ );
483
+ const { outputPath } = await saveDocument({
484
+ scenario,
485
+ scenarioId,
486
+ filename,
487
+ content,
488
+ formData
489
+ });
490
+ if (scenario.hooks?.onSave != null) {
491
+ await scenario.hooks.onSave({
492
+ content,
493
+ filename,
494
+ outputPath,
495
+ formData,
496
+ inputData
497
+ });
498
+ }
499
+ return c.json({ success: true, filename });
500
+ });
501
+ app.get("/api/scenarios/:scenarioId/docs/:filename", async (c) => {
502
+ const filename = c.req.param("filename");
503
+ const { scenario } = c.get("scenarioInfo");
504
+ const result = await readDocument(scenario, filename);
505
+ if (!result.success) {
506
+ return c.json({ error: "Document not found" }, 404);
507
+ }
508
+ return c.json({ doc: result.doc });
509
+ });
510
+ app.put("/api/scenarios/:scenarioId/docs/:filename", async (c) => {
511
+ const scenarioId = c.req.param("scenarioId");
512
+ const filename = c.req.param("filename");
513
+ const { scenario, sectionInfoMap } = c.get("scenarioInfo");
514
+ if (!config.permissions.allowSave) {
515
+ return c.json({ error: "Save is not allowed" }, 403);
516
+ }
517
+ const { content, formData } = await c.req.json();
518
+ const inputData = transformFormData(formData, sectionInfoMap);
519
+ const { outputPath } = await saveDocument({
520
+ scenario,
521
+ scenarioId,
522
+ filename,
523
+ content,
524
+ formData
525
+ });
526
+ if (scenario.hooks?.onSave != null) {
527
+ await scenario.hooks.onSave({
528
+ content,
529
+ filename,
530
+ outputPath,
531
+ formData,
532
+ inputData
533
+ });
534
+ }
535
+ return c.json({ success: true, filename });
536
+ });
537
+ return app;
538
+ };
539
+
540
+ // src/server/apps/scenarios.ts
541
+ import { Hono as Hono2 } from "hono";
542
+
543
+ // src/server/helpers/scenarios/build-form-defaults.ts
544
+ var buildFieldDefaults = (fields) => {
545
+ const getFieldDefaultValue = (field) => {
546
+ if (isLayoutField(field)) {
547
+ return void 0;
548
+ }
549
+ switch (field.type) {
550
+ case "checkbox":
551
+ return false;
552
+ default:
553
+ return "";
554
+ }
555
+ };
556
+ const defaults = {};
557
+ for (const field of fields) {
558
+ if (isLayoutField(field)) {
559
+ Object.assign(defaults, buildFieldDefaults(field.fields));
560
+ } else {
561
+ defaults[field.id] = getFieldDefaultValue(field);
562
+ }
563
+ }
564
+ return defaults;
565
+ };
566
+ var buildFormDefaultValues = (steps) => {
567
+ const defaults = {};
568
+ for (const config of steps) {
569
+ if (config.section.type === "single") {
570
+ defaults[config.section.name] = buildFieldDefaults(config.section.fields);
571
+ } else {
572
+ const minCount = config.section.minFieldCount ?? 1;
573
+ defaults[config.section.name] = Array.from(
574
+ { length: minCount },
575
+ () => buildFieldDefaults(config.section.fields)
576
+ );
577
+ }
578
+ }
579
+ return defaults;
580
+ };
581
+
582
+ // src/server/apps/scenarios.ts
583
+ var createScenariosApp = (config, scenarioInfoMap) => {
584
+ const app = new Hono2();
585
+ app.get("/api/scenarios", (c) => {
586
+ return c.json({
587
+ scenarios: config.scenarios
588
+ });
589
+ });
590
+ app.get("/api/scenarios/:scenarioId", (c) => {
591
+ const scenarioId = c.req.param("scenarioId");
592
+ const scenarioInfo = scenarioInfoMap.get(scenarioId);
593
+ if (scenarioInfo == null) {
594
+ return c.json({ error: "Scenario not found" }, 404);
595
+ }
596
+ const formDefaultValues = buildFormDefaultValues(
597
+ scenarioInfo.scenario.steps
598
+ );
599
+ return c.json({
600
+ scenario: scenarioInfo.scenario,
601
+ formDefaultValues,
602
+ permissions: config.permissions
603
+ });
604
+ });
605
+ return app;
606
+ };
607
+
608
+ // src/server/helpers/scenarios/build-section-info.ts
609
+ var extractFieldInfos = (fields) => {
610
+ const result = [];
611
+ for (const field of fields) {
612
+ if (isLayoutField(field)) {
613
+ result.push(...extractFieldInfos(field.fields));
614
+ } else {
615
+ result.push({
616
+ id: field.id,
617
+ label: field.label,
618
+ description: field.description
619
+ });
620
+ }
621
+ }
622
+ return result;
623
+ };
624
+ var buildSectionInfoMap = (steps) => {
625
+ const sectionMap = new Map(
626
+ steps.map((step) => [
627
+ step.section.name,
628
+ {
629
+ name: step.section.name,
630
+ title: step.title,
631
+ description: step.description,
632
+ fields: extractFieldInfos(step.section.fields)
633
+ }
634
+ ])
635
+ );
636
+ return sectionMap;
637
+ };
638
+
639
+ // src/server/api.ts
640
+ var createApiServer = (config) => {
641
+ const app = new Hono3();
642
+ const scenarioInfoMap = new Map(
643
+ config.scenarios.map((scenario) => [
644
+ scenario.id,
645
+ {
646
+ scenario,
647
+ sectionInfoMap: buildSectionInfoMap(scenario.steps)
648
+ }
649
+ ])
650
+ );
651
+ const scenariosApp = createScenariosApp(config, scenarioInfoMap);
652
+ const docsApp = createDocsApp(config, scenarioInfoMap);
653
+ app.route("/", scenariosApp);
654
+ app.route("/", docsApp);
655
+ return app;
656
+ };
657
+
658
+ // src/cli/commands/start.ts
659
+ var getDistClientDir = () => {
660
+ const __filename = url.fileURLToPath(import.meta.url);
661
+ const __dirname = path2.dirname(__filename);
662
+ const isBuilt = __dirname.endsWith("/dist") || __dirname.includes("/dist/");
663
+ return isBuilt ? path2.resolve(__dirname, "client") : path2.resolve(__dirname, "../../../dist/client");
664
+ };
665
+ var loadConfig = async (configPath) => {
666
+ const absolutePath = path2.resolve(process.cwd(), configPath);
667
+ if (!fs2.existsSync(absolutePath)) {
668
+ throw new Error(`Config file not found: ${absolutePath}`);
669
+ }
670
+ const jiti = createJiti(import.meta.url);
671
+ const configModule = await jiti.import(absolutePath);
672
+ const config = configModule.default;
673
+ const result = safeParseConfig(config);
674
+ if (!result.success) {
675
+ const issues = result.issues.map((issue) => {
676
+ const pathStr = issue.path?.map((p) => p.key).join(".") ?? "";
677
+ return ` - ${pathStr}: ${issue.message}`;
678
+ });
679
+ throw new Error(`Invalid config:
680
+ ${issues.join("\n")}`);
681
+ }
682
+ return config;
683
+ };
684
+ var runServer = async (config, options) => {
685
+ consola2.start("Starting server...");
686
+ const app = createApiServer(config);
687
+ app.use(
688
+ "/*",
689
+ serveStatic({
690
+ root: options.distDir,
691
+ rewriteRequestPath: (requestPath) => {
692
+ const fullPath = path2.join(options.distDir, requestPath);
693
+ if (fs2.existsSync(fullPath) && fs2.statSync(fullPath).isFile()) {
694
+ return requestPath;
695
+ }
696
+ return "/index.html";
697
+ }
698
+ })
699
+ );
700
+ const server = serve(
701
+ {
702
+ fetch: app.fetch,
703
+ port: options.port,
704
+ hostname: options.host
705
+ },
706
+ (info) => {
707
+ const displayHost = info.address === "::1" || info.address === "127.0.0.1" ? "localhost" : info.address;
708
+ consola2.success("Server started");
709
+ consola2.info(` \u279C Local: http://${displayHost}:${info.port}/`);
710
+ }
711
+ );
712
+ const cleanup = () => {
713
+ consola2.info("Shutting down server...");
714
+ server.close(() => {
715
+ process.exit(0);
716
+ });
717
+ };
718
+ process.on("SIGINT", cleanup);
719
+ process.on("SIGTERM", cleanup);
720
+ };
721
+ var startCommand = defineCommand2({
722
+ meta: {
723
+ name: "start",
724
+ description: "Start the server with the specified config"
725
+ },
726
+ args: {
727
+ config: {
728
+ type: "string",
729
+ description: "Path to config file",
730
+ alias: "c",
731
+ default: "spec-snake.config.ts"
732
+ },
733
+ port: {
734
+ type: "string",
735
+ description: "Port to run the server on",
736
+ alias: "p",
737
+ default: "3000"
738
+ },
739
+ host: {
740
+ type: "string",
741
+ description: "Host to bind the server to",
742
+ default: "localhost"
743
+ }
744
+ },
745
+ async run({ args }) {
746
+ const configPath = args.config;
747
+ const port = Number.parseInt(args.port, 10);
748
+ consola2.start(`Loading config from: ${configPath}`);
749
+ try {
750
+ const config = await loadConfig(configPath);
751
+ consola2.success(
752
+ `Config loaded successfully (${config.scenarios.length} scenarios)`
753
+ );
754
+ await runServer(config, {
755
+ distDir: getDistClientDir(),
756
+ port,
757
+ host: args.host
758
+ });
759
+ } catch (error) {
760
+ if (error instanceof Error) {
761
+ consola2.error(error.message);
762
+ } else {
763
+ consola2.error("Failed to start server:", error);
764
+ }
765
+ process.exit(1);
766
+ }
767
+ }
768
+ });
769
+
770
+ // src/cli/index.ts
771
+ var main = defineCommand3({
772
+ meta: {
773
+ name: "design-docs-generator",
774
+ version: "0.0.1",
775
+ description: "Design Docs Generator CLI"
776
+ },
777
+ subCommands: {
778
+ init: initCommand,
779
+ start: startCommand
780
+ }
781
+ });
782
+ runMain(main);
783
+ //# sourceMappingURL=cli.js.map