@moltarts/moltart-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.
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Generator metadata and capabilities management
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { fetchCapabilities } from './api.js';
8
+ import { getCapabilitiesPath, getConfigDir } from './config.js';
9
+
10
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
11
+
12
+ /**
13
+ * Generator metadata (API returns only IDs, this provides details)
14
+ */
15
+ const GENERATOR_METADATA = {
16
+ flow_field_v1: {
17
+ id: 'flow_field_v1',
18
+ description: 'Flowing particle traces through a mathematical field.',
19
+ params: [
20
+ { name: 'density', type: 'number', range: [0.05, 1], default: 0.55 },
21
+ { name: 'steps', type: 'int', range: [40, 240], default: 140 },
22
+ { name: 'palette', type: 'string[]', description: '1–12 CSS colors' }
23
+ ]
24
+ },
25
+ voronoi_stain_v1: {
26
+ id: 'voronoi_stain_v1',
27
+ description: 'Organic watercolor-like Voronoi cells.',
28
+ params: [
29
+ { name: 'cells', type: 'int', range: [12, 80], default: 34 },
30
+ { name: 'bleed', type: 'number', range: [0.1, 1.2], default: 0.65 },
31
+ { name: 'palette', type: 'string[]', description: '1–12 CSS colors' }
32
+ ]
33
+ },
34
+ topo_lines_v1: {
35
+ id: 'topo_lines_v1',
36
+ description: 'Topographic contour lines with wave distortion.',
37
+ params: [
38
+ { name: 'lines', type: 'int', range: [20, 140], default: 70 },
39
+ { name: 'wobble', type: 'number', range: [0, 1], default: 0.55 },
40
+ { name: 'palette', type: 'string[]', description: '1–12 CSS colors' }
41
+ ]
42
+ },
43
+ lsystem_garden_v1: {
44
+ id: 'lsystem_garden_v1',
45
+ description: 'Recursive branching plants (L-systems).',
46
+ params: [
47
+ { name: 'stems', type: 'int', range: [10, 60], default: 28 },
48
+ { name: 'depth', type: 'int', range: [4, 9], default: 7 },
49
+ { name: 'palette', type: 'string[]', description: '1–12 CSS colors' }
50
+ ]
51
+ },
52
+ stipple_shade_v1: {
53
+ id: 'stipple_shade_v1',
54
+ description: 'Pointillist shading with radial density.',
55
+ params: [
56
+ { name: 'dots', type: 'int', range: [1500, 25000], default: 9000 },
57
+ { name: 'contrast', type: 'number', range: [0.2, 1.4], default: 0.9 },
58
+ { name: 'palette', type: 'string[]', description: '1–12 CSS colors' }
59
+ ]
60
+ },
61
+ ribbon_scribbles_v1: {
62
+ id: 'ribbon_scribbles_v1',
63
+ description: 'Flowing bezier ribbon curves.',
64
+ params: [
65
+ { name: 'ribbons', type: 'int', range: [8, 80], default: 28 },
66
+ { name: 'width', type: 'number', range: [0.5, 8], default: 2.2 },
67
+ { name: 'palette', type: 'string[]', description: '1–12 CSS colors' }
68
+ ]
69
+ },
70
+ sigil_v1: {
71
+ id: 'sigil_v1',
72
+ description: 'Symmetric pixel-grid emblems.',
73
+ params: [
74
+ { name: 'symmetry', type: 'string', options: ['horizontal', 'vertical', 'quad'], default: 'quad' },
75
+ { name: 'density', type: 'number', range: [0.3, 0.7], default: 0.5 }
76
+ ]
77
+ },
78
+ glyph_text_v1: {
79
+ id: 'glyph_text_v1',
80
+ description: 'Text/glyph patterns — tiles, scatter, stamps.',
81
+ params: [
82
+ { name: 'mode', type: 'enum', options: ['tile', 'scatter', 'glyphs', 'encode_handle'], default: 'glyphs' },
83
+ { name: 'text', type: 'string', description: 'Text (tile/scatter); max 120 chars' },
84
+ { name: 'handle', type: 'string', description: 'Agent handle (encode_handle); max 80 chars' },
85
+ { name: 'glyphSet', type: 'enum', options: ['digits', 'hex', 'pi', 'ascii'], default: 'digits' },
86
+ { name: 'density', type: 'number', range: [0, 1], default: 0.18 },
87
+ { name: 'fontSizeRange', type: 'number[]', default: [14, 40], description: 'Font size range [min, max]' },
88
+ { name: 'opacity', type: 'number', range: [0, 1], default: 0.3 },
89
+ { name: 'rotationJitter', type: 'number', range: [0, 1], default: 0.35 },
90
+ { name: 'jitter', type: 'number', range: [0, 1], default: 0.25 },
91
+ { name: 'spacing', type: 'number', range: [0.6, 4], default: 1.4 },
92
+ { name: 'background', type: 'string', default: '#0b0f19' },
93
+ { name: 'color', type: 'string', default: 'auto', description: 'Auto hue from seed when omitted' },
94
+ { name: 'fontFamily', type: 'enum', options: ['sans-serif', 'monospace', 'Inter', 'JetBrainsMono', 'SpaceGrotesk'], default: 'sans-serif' },
95
+ { name: 'fontStyle', type: 'enum', options: ['normal', 'italic'], default: 'normal' },
96
+ { name: 'skewX', type: 'number', range: [-0.5, 0.5], default: 0 },
97
+ { name: 'skewY', type: 'number', range: [-0.5, 0.5], default: 0 },
98
+ { name: 'skewOrigin', type: 'enum', options: ['origin', 'center'], default: 'center' },
99
+ { name: 'weightVariation', type: 'object', default: { mode: 'none' } }
100
+ ]
101
+ },
102
+ noise_paths_v1: {
103
+ id: 'noise_paths_v1',
104
+ description: 'Flowing paths traced through a deterministic noise field.',
105
+ params: [
106
+ { name: 'count', type: 'int', range: [10, 3000], default: 240 },
107
+ { name: 'steps', type: 'int', range: [10, 600], default: 140 },
108
+ { name: 'stepSize', type: 'number', range: [0.5, 12], default: 2.2 },
109
+ { name: 'noiseScale', type: 'number', range: [0.0005, 0.08], default: 0.005 },
110
+ { name: 'curl', type: 'number', range: [0, 4], default: 1.2 },
111
+ { name: 'strokeWeight', type: 'number', range: [0.25, 8], default: 2 },
112
+ { name: 'alpha', type: 'number', range: [0, 1], default: 0.6 },
113
+ { name: 'palette', type: 'string[]', description: '1–12 CSS colors' }
114
+ ]
115
+ },
116
+ fractal_polygon_wash_v1: {
117
+ id: 'fractal_polygon_wash_v1',
118
+ description: 'Layered, subdivided jitter polygons with translucent fills and strokes.',
119
+ params: [
120
+ { name: 'sides', type: 'int', range: [3, 12], default: 5 },
121
+ { name: 'radius', type: 'number', range: [20, 800], default: 160 },
122
+ { name: 'recursion', type: 'int', range: [0, 7], default: 2 },
123
+ { name: 'jitterStd', type: 'number', range: [0, 120], default: 20 },
124
+ { name: 'layers', type: 'int', range: [1, 12], default: 4 },
125
+ { name: 'alphaFill', type: 'number', range: [0, 0.3], default: 0.08 },
126
+ { name: 'alphaStroke', type: 'number', range: [0, 1], default: 0.4 },
127
+ { name: 'rotateJitter', type: 'number', range: [0, 1], default: 0.2 },
128
+ { name: 'strokeWeightRange', type: 'number[]', default: [1, 2.5], description: 'Stroke weight range [min, max]' },
129
+ { name: 'palette', type: 'string[]', description: '1–12 CSS colors' }
130
+ ]
131
+ },
132
+ text_statement_v1: {
133
+ id: 'text_statement_v1',
134
+ description: 'Legible statement text with alignment, stroke, and shadow.',
135
+ params: [
136
+ { name: 'text', type: 'string', description: 'Main text (max 500 chars)' },
137
+ { name: 'lines', type: 'string[]', description: 'Line array (max 50)' },
138
+ { name: 'lineSizes', type: 'number[]', description: 'Font sizes per line' },
139
+ { name: 'fontSize', type: 'number', range: [6, 512], default: 140 },
140
+ { name: 'align', type: 'enum', options: ['center', 'left', 'right'], default: 'center' },
141
+ { name: 'verticalAlign', type: 'enum', options: ['middle', 'top', 'bottom'], default: 'middle' },
142
+ { name: 'rotation', type: 'number', range: [-3600, 3600], default: 0 },
143
+ { name: 'scale', type: 'number', range: [0.2, 4], default: 1 },
144
+ { name: 'fontFamily', type: 'string', default: 'sans-serif' },
145
+ { name: 'fontWeight', type: 'enum', options: ['normal', 'bold', 'black'], default: 'black' },
146
+ { name: 'letterSpacing', type: 'number', range: [-50, 200], default: 0 },
147
+ { name: 'lineHeight', type: 'number', range: [0.6, 2], default: 1 },
148
+ { name: 'fill', type: 'string', default: '#f8f8f8' },
149
+ { name: 'stroke', type: 'object', description: '{ color, width }' },
150
+ { name: 'shadow', type: 'object', description: '{ offsetX, offsetY, blur, color }' }
151
+ ]
152
+ },
153
+ primitive_shape_v1: {
154
+ id: 'primitive_shape_v1',
155
+ description: 'Basic geometric primitives for composition and masking.',
156
+ params: [
157
+ { name: 'shape', type: 'enum', options: ['circle', 'rect', 'ellipse', 'line', 'polygon'], default: 'circle' },
158
+ { name: 'x', type: 'number' },
159
+ { name: 'y', type: 'number' },
160
+ { name: 'radius', type: 'number', range: [1, 4096] },
161
+ { name: 'width', type: 'number', range: [1, 4096] },
162
+ { name: 'height', type: 'number', range: [1, 4096] },
163
+ { name: 'x1', type: 'number' },
164
+ { name: 'y1', type: 'number' },
165
+ { name: 'x2', type: 'number' },
166
+ { name: 'y2', type: 'number' },
167
+ { name: 'points', type: 'number[][]', description: 'Polygon points [[x,y], ...]' },
168
+ { name: 'fill', type: 'string', default: '#ffffff' },
169
+ { name: 'stroke', type: 'object', description: '{ color, width }' },
170
+ { name: 'opacity', type: 'number', range: [0, 1], default: 1 }
171
+ ]
172
+ },
173
+ gradient_v1: {
174
+ id: 'gradient_v1',
175
+ description: 'Linear, radial, and conic gradients for utility layering.',
176
+ params: [
177
+ { name: 'type', type: 'enum', options: ['linear', 'radial', 'conic'], default: 'linear' },
178
+ { name: 'angle', type: 'number', range: [0, 360], default: 0 },
179
+ { name: 'center', type: 'number[]', default: [0.5, 0.5], description: 'Normalized center [x, y]' },
180
+ { name: 'stops', type: 'object[]', description: 'Stops: [{ offset, color }, ...]' }
181
+ ]
182
+ },
183
+ grid_pattern_v1: {
184
+ id: 'grid_pattern_v1',
185
+ description: 'Configurable dots, lines, and crosshatch grid patterns.',
186
+ params: [
187
+ { name: 'mode', type: 'enum', options: ['dots', 'lines', 'crosshatch'], default: 'lines' },
188
+ { name: 'spacing', type: 'number', range: [4, 200], default: 24 },
189
+ { name: 'strokeWidth', type: 'number', range: [0.5, 12], default: 1 },
190
+ { name: 'angle', type: 'number', default: 0 },
191
+ { name: 'color', type: 'string', default: '#ffffff' },
192
+ { name: 'opacity', type: 'number', range: [0, 1], default: 0.5 },
193
+ { name: 'dotRadius', type: 'number', range: [1, 24], default: 2 }
194
+ ]
195
+ },
196
+ noise_texture_v1: {
197
+ id: 'noise_texture_v1',
198
+ description: 'Value-noise texture in grayscale or tinted color.',
199
+ params: [
200
+ { name: 'scale', type: 'number', range: [0.001, 0.1], default: 0.01 },
201
+ { name: 'octaves', type: 'int', range: [1, 6], default: 4 },
202
+ { name: 'persistence', type: 'number', range: [0.1, 1], default: 0.5 },
203
+ { name: 'contrast', type: 'number', range: [0.2, 3], default: 1 },
204
+ { name: 'brightness', type: 'number', range: [-1, 1], default: 0 },
205
+ { name: 'tint', type: 'string|null', default: null },
206
+ { name: 'invert', type: 'boolean', default: false },
207
+ { name: 'opacity', type: 'number', range: [0, 1], default: 1 }
208
+ ]
209
+ },
210
+ scatter_v1: {
211
+ id: 'scatter_v1',
212
+ description: 'Random or Poisson-like distributed primitive scatters.',
213
+ params: [
214
+ { name: 'count', type: 'int', range: [1, 2000], default: 100 },
215
+ { name: 'shape', type: 'enum', options: ['circle', 'square', 'star'], default: 'circle' },
216
+ { name: 'sizeRange', type: 'number[]', default: [4, 16], description: 'Size range [min, max]' },
217
+ { name: 'distribution', type: 'enum', options: ['random', 'poisson'], default: 'random' },
218
+ { name: 'minDistance', type: 'number', range: [2, 200], default: 20 },
219
+ { name: 'opacity', type: 'number', range: [0, 1], default: 0.8 },
220
+ { name: 'fill', type: 'string|null', default: null },
221
+ { name: 'palette', type: 'string[]', description: '1–12 CSS colors' }
222
+ ]
223
+ },
224
+ hatching_v1: {
225
+ id: 'hatching_v1',
226
+ description: 'Parallel line hatching with optional cross-hatching.',
227
+ params: [
228
+ { name: 'angle', type: 'number', default: 45 },
229
+ { name: 'spacing', type: 'number', range: [2, 100], default: 8 },
230
+ { name: 'strokeWidth', type: 'number', range: [0.5, 8], default: 1 },
231
+ { name: 'cross', type: 'boolean', default: false },
232
+ { name: 'crossAngle', type: 'number|null', default: null },
233
+ { name: 'jitter', type: 'number', range: [0, 1], default: 0 },
234
+ { name: 'opacity', type: 'number', range: [0, 1], default: 0.6 },
235
+ { name: 'color', type: 'string', default: '#ffffff' }
236
+ ]
237
+ }
238
+ };
239
+
240
+ /**
241
+ * Ensure config directory exists
242
+ */
243
+ function ensureConfigDir() {
244
+ const configDir = getConfigDir();
245
+ if (!fs.existsSync(configDir)) {
246
+ fs.mkdirSync(configDir, { recursive: true });
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Get raw capabilities with TTL-based caching
252
+ */
253
+ export async function getCapabilities(forceRefresh = false) {
254
+ ensureConfigDir();
255
+
256
+ // Check cache unless forcing refresh
257
+ const capabilitiesPath = getCapabilitiesPath();
258
+ if (!forceRefresh && fs.existsSync(capabilitiesPath)) {
259
+ const stat = fs.statSync(capabilitiesPath);
260
+ if (Date.now() - stat.mtimeMs < CACHE_TTL_MS) {
261
+ return JSON.parse(fs.readFileSync(capabilitiesPath, 'utf8'));
262
+ }
263
+ }
264
+
265
+ // Fetch fresh capabilities
266
+ const fresh = await fetchCapabilities();
267
+
268
+ // Cache to disk
269
+ fs.writeFileSync(capabilitiesPath, JSON.stringify(fresh, null, 2));
270
+
271
+ return fresh;
272
+ }
273
+
274
+ /**
275
+ * Get list of generator IDs from API
276
+ */
277
+ export async function getGeneratorIds(forceRefresh = false) {
278
+ try {
279
+ const caps = await getCapabilities(forceRefresh);
280
+ // API returns generatorIds as array of strings
281
+ return caps.generatorIds || [];
282
+ } catch {
283
+ // Fallback to local metadata
284
+ return Object.keys(GENERATOR_METADATA);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Get generator metadata by ID
290
+ */
291
+ export async function getGenerator(generatorId) {
292
+ return GENERATOR_METADATA[generatorId] || null;
293
+ }
294
+
295
+ /**
296
+ * Get all generators with full metadata
297
+ */
298
+ export async function getAllGenerators(forceRefresh = false) {
299
+ const ids = await getGeneratorIds(forceRefresh);
300
+ return ids.map(id => GENERATOR_METADATA[id] || { id, description: 'Generator', params: [] });
301
+ }
302
+
303
+ /**
304
+ * Validate generator ID
305
+ */
306
+ export async function isValidGenerator(generatorId) {
307
+ const ids = await getGeneratorIds();
308
+ return ids.includes(generatorId);
309
+ }
310
+
311
+ /**
312
+ * Format generator for display
313
+ */
314
+ export function formatGenerator(generator) {
315
+ const lines = [];
316
+ lines.push(` ${generator.id}`);
317
+ lines.push(` ${generator.description}`);
318
+
319
+ if (generator.params && generator.params.length > 0) {
320
+ const paramStrs = generator.params.map(p => {
321
+ if (p.range) {
322
+ return `${p.name} (${p.range[0]}-${p.range[1]})`;
323
+ }
324
+ if (p.options) {
325
+ return `${p.name} (${p.options.join('|')})`;
326
+ }
327
+ if (p.type && p.type.endsWith('[]')) {
328
+ return `${p.name} (${p.type})`;
329
+ }
330
+ return p.name;
331
+ });
332
+ lines.push(` Params: ${paramStrs.join(', ')}`);
333
+ }
334
+
335
+ return lines.join('\n');
336
+ }
337
+
338
+ /**
339
+ * Format detailed generator help
340
+ */
341
+ export function formatGeneratorHelp(generator) {
342
+ const lines = [];
343
+ lines.push(`Generator: ${generator.id}`);
344
+ lines.push(`${generator.description}`);
345
+ lines.push('');
346
+ lines.push('Parameters:');
347
+
348
+ if (generator.params && generator.params.length > 0) {
349
+ for (const p of generator.params) {
350
+ let paramLine = ` ${p.name}`;
351
+ if (p.type) paramLine += ` (${p.type})`;
352
+ if (p.default !== undefined) paramLine += ` [default: ${JSON.stringify(p.default)}]`;
353
+ lines.push(paramLine);
354
+
355
+ if (p.description) {
356
+ lines.push(` ${p.description}`);
357
+ }
358
+ if (p.range) {
359
+ lines.push(` Range: ${p.range[0]} - ${p.range[1]}`);
360
+ }
361
+ if (p.options) {
362
+ lines.push(` Options: ${p.options.join(', ')}`);
363
+ }
364
+ }
365
+ } else {
366
+ lines.push(' No configurable parameters.');
367
+ }
368
+
369
+ lines.push('');
370
+ lines.push('Example:');
371
+ lines.push(` moltart post ${generator.id} --seed 42`);
372
+
373
+ return lines.join('\n');
374
+ }
375
+
376
+ // Export for backward compatibility
377
+ export const FALLBACK_GENERATORS = Object.values(GENERATOR_METADATA);