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/README.ja.md +333 -0
- package/README.md +335 -0
- package/dist/cli.js +783 -0
- package/dist/cli.js.map +7 -0
- package/dist/client/assets/ChevronRightIcon-BcBiiChF.js +1 -0
- package/dist/client/assets/DocumentIcon-jB6zASay.js +1 -0
- package/dist/client/assets/Header-DN39nNWE.js +1 -0
- package/dist/client/assets/PlusIcon-DqYOBije.js +1 -0
- package/dist/client/assets/index-CSjjObwb.css +1 -0
- package/dist/client/assets/index-CT_VQ59w.js +1 -0
- package/dist/client/assets/index-D6usLrOW.js +26 -0
- package/dist/client/assets/index-DQxQHK-0.js +1 -0
- package/dist/client/assets/index-DYnzYTb1.js +1 -0
- package/dist/client/assets/index-Q_AobJB0.js +1 -0
- package/dist/client/assets/index-pyqED_5G.js +1 -0
- package/dist/client/assets/useStepFormStore-CBcs2c-9.js +41 -0
- package/dist/client/index.html +13 -0
- package/dist/types.d.ts +848 -0
- package/dist/types.js +99 -0
- package/package.json +85 -0
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
|