@genart-dev/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +386 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2012 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2012 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
import { Command as Command16 } from "commander";
|
|
11
|
+
|
|
12
|
+
// src/commands/render.ts
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
import { resolve, basename, extname } from "path";
|
|
15
|
+
import { writeFile } from "fs/promises";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import ora from "ora";
|
|
18
|
+
import { createDefaultRegistry } from "@genart-dev/core";
|
|
19
|
+
|
|
20
|
+
// src/util/load-sketch.ts
|
|
21
|
+
import { readFile } from "fs/promises";
|
|
22
|
+
import { parseGenart } from "@genart-dev/format";
|
|
23
|
+
async function loadSketch(filePath) {
|
|
24
|
+
const raw = await readFile(filePath, "utf-8");
|
|
25
|
+
const json = JSON.parse(raw);
|
|
26
|
+
return parseGenart(json);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/util/apply-overrides.ts
|
|
30
|
+
import {
|
|
31
|
+
resolvePreset
|
|
32
|
+
} from "@genart-dev/format";
|
|
33
|
+
function applyOverrides(sketch, overrides) {
|
|
34
|
+
let canvas = { ...sketch.canvas };
|
|
35
|
+
let state = { ...sketch.state, params: { ...sketch.state.params } };
|
|
36
|
+
if (overrides.preset) {
|
|
37
|
+
const dims = resolvePreset(overrides.preset);
|
|
38
|
+
canvas = { ...canvas, preset: overrides.preset, width: dims.width, height: dims.height };
|
|
39
|
+
}
|
|
40
|
+
if (overrides.width !== void 0) canvas = { ...canvas, width: overrides.width };
|
|
41
|
+
if (overrides.height !== void 0) canvas = { ...canvas, height: overrides.height };
|
|
42
|
+
if (overrides.seed !== void 0) {
|
|
43
|
+
state = { ...state, seed: overrides.seed };
|
|
44
|
+
}
|
|
45
|
+
if (overrides.params) {
|
|
46
|
+
state = { ...state, params: { ...state.params, ...overrides.params } };
|
|
47
|
+
}
|
|
48
|
+
if (overrides.colors) {
|
|
49
|
+
state = { ...state, colorPalette: overrides.colors };
|
|
50
|
+
}
|
|
51
|
+
return { ...sketch, canvas, state };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/util/parse-wait.ts
|
|
55
|
+
function parseWait(value) {
|
|
56
|
+
const trimmed = value.trim().toLowerCase();
|
|
57
|
+
if (trimmed.endsWith("ms")) {
|
|
58
|
+
return Math.max(0, Number(trimmed.slice(0, -2)));
|
|
59
|
+
}
|
|
60
|
+
if (trimmed.endsWith("s")) {
|
|
61
|
+
return Math.max(0, Number(trimmed.slice(0, -1)) * 1e3);
|
|
62
|
+
}
|
|
63
|
+
const num = Number(trimmed);
|
|
64
|
+
if (Number.isNaN(num)) {
|
|
65
|
+
throw new Error(`Invalid wait value: "${value}". Use "500ms" or "2s".`);
|
|
66
|
+
}
|
|
67
|
+
return Math.max(0, num);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/capture/browser.ts
|
|
71
|
+
import puppeteer from "puppeteer-core";
|
|
72
|
+
import { existsSync } from "fs";
|
|
73
|
+
var CHROME_PATHS = {
|
|
74
|
+
darwin: [
|
|
75
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
76
|
+
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
77
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
78
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
79
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
|
|
80
|
+
],
|
|
81
|
+
linux: [
|
|
82
|
+
"/usr/bin/google-chrome",
|
|
83
|
+
"/usr/bin/google-chrome-stable",
|
|
84
|
+
"/usr/bin/chromium",
|
|
85
|
+
"/usr/bin/chromium-browser",
|
|
86
|
+
"/snap/bin/chromium"
|
|
87
|
+
],
|
|
88
|
+
win32: [
|
|
89
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
90
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
function findChromePath() {
|
|
94
|
+
const envPath = process.env["GENART_CHROME_PATH"];
|
|
95
|
+
if (envPath && existsSync(envPath)) return envPath;
|
|
96
|
+
const paths = CHROME_PATHS[process.platform];
|
|
97
|
+
if (!paths) return void 0;
|
|
98
|
+
for (const p of paths) {
|
|
99
|
+
if (existsSync(p)) return p;
|
|
100
|
+
}
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
var browserInstance = null;
|
|
104
|
+
async function getBrowser() {
|
|
105
|
+
if (browserInstance?.connected) {
|
|
106
|
+
return browserInstance;
|
|
107
|
+
}
|
|
108
|
+
const executablePath = findChromePath();
|
|
109
|
+
if (!executablePath) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"No Chrome/Chromium found. Install Google Chrome or set GENART_CHROME_PATH.\n macOS: brew install --cask google-chrome\n Linux: sudo apt install google-chrome-stable\n Or: GENART_CHROME_PATH=/path/to/chrome genart render ..."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
browserInstance = await puppeteer.launch({
|
|
115
|
+
headless: true,
|
|
116
|
+
executablePath,
|
|
117
|
+
args: [
|
|
118
|
+
"--no-sandbox",
|
|
119
|
+
"--disable-setuid-sandbox",
|
|
120
|
+
"--disable-gpu",
|
|
121
|
+
"--disable-dev-shm-usage"
|
|
122
|
+
]
|
|
123
|
+
});
|
|
124
|
+
return browserInstance;
|
|
125
|
+
}
|
|
126
|
+
async function captureHtml(options) {
|
|
127
|
+
const {
|
|
128
|
+
html,
|
|
129
|
+
width,
|
|
130
|
+
height,
|
|
131
|
+
waitMs = 500,
|
|
132
|
+
format = "png",
|
|
133
|
+
quality = 80,
|
|
134
|
+
scale = 1
|
|
135
|
+
} = options;
|
|
136
|
+
const browser = await getBrowser();
|
|
137
|
+
const page = await browser.newPage();
|
|
138
|
+
try {
|
|
139
|
+
await page.setViewport({ width, height, deviceScaleFactor: scale });
|
|
140
|
+
await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
141
|
+
if (waitMs > 0) {
|
|
142
|
+
await new Promise((resolve10) => setTimeout(resolve10, waitMs));
|
|
143
|
+
}
|
|
144
|
+
const screenshotType = format === "webp" ? "webp" : format;
|
|
145
|
+
const buffer = await page.screenshot({
|
|
146
|
+
type: screenshotType,
|
|
147
|
+
clip: { x: 0, y: 0, width, height },
|
|
148
|
+
...screenshotType !== "png" ? { quality } : {}
|
|
149
|
+
});
|
|
150
|
+
const bytes = new Uint8Array(buffer);
|
|
151
|
+
const mimeType = `image/${format}`;
|
|
152
|
+
return { bytes, mimeType, width, height };
|
|
153
|
+
} finally {
|
|
154
|
+
await page.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function getPage(width, height, scale = 1) {
|
|
158
|
+
const browser = await getBrowser();
|
|
159
|
+
const page = await browser.newPage();
|
|
160
|
+
await page.setViewport({ width, height, deviceScaleFactor: scale });
|
|
161
|
+
return page;
|
|
162
|
+
}
|
|
163
|
+
async function closeBrowser() {
|
|
164
|
+
if (browserInstance) {
|
|
165
|
+
await browserInstance.close();
|
|
166
|
+
browserInstance = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/commands/render.ts
|
|
171
|
+
var renderCommand = new Command("render").description("Render a .genart sketch to an image").argument("<file>", "Path to .genart file").option("--wait <duration>", "How long to let the sketch animate before capture", "500ms").option("--seed <n>", "Override seed", Number).option("--params <json>", "Override parameters (JSON object)").option("--colors <json>", "Override color palette (JSON array of hex strings)").option("--width <n>", "Override canvas width", Number).option("--height <n>", "Override canvas height", Number).option("--preset <name>", "Use a canvas preset").option("--format <fmt>", "Output format: png, jpeg, webp", "png").option("--quality <n>", "Lossy compression quality (0-100)", Number, 80).option("--scale <n>", "Pixel density multiplier", Number, 1).option("-o, --output <path>", "Output file path").action(async (file, opts) => {
|
|
172
|
+
const spinner = ora("Loading sketch...").start();
|
|
173
|
+
try {
|
|
174
|
+
const filePath = resolve(file);
|
|
175
|
+
const sketch = await loadSketch(filePath);
|
|
176
|
+
const overrides = {};
|
|
177
|
+
if (opts.seed !== void 0) overrides.seed = opts.seed;
|
|
178
|
+
if (opts.width !== void 0) overrides.width = opts.width;
|
|
179
|
+
if (opts.height !== void 0) overrides.height = opts.height;
|
|
180
|
+
if (opts.preset) overrides.preset = opts.preset;
|
|
181
|
+
if (opts.params) {
|
|
182
|
+
overrides.params = JSON.parse(opts.params);
|
|
183
|
+
}
|
|
184
|
+
if (opts.colors) {
|
|
185
|
+
overrides.colors = JSON.parse(opts.colors);
|
|
186
|
+
}
|
|
187
|
+
const modified = applyOverrides(sketch, overrides);
|
|
188
|
+
spinner.text = "Generating standalone HTML...";
|
|
189
|
+
const registry = createDefaultRegistry();
|
|
190
|
+
const adapter = registry.resolve(modified.renderer.type);
|
|
191
|
+
const html = adapter.generateStandaloneHTML(modified);
|
|
192
|
+
const waitMs = parseWait(opts.wait);
|
|
193
|
+
const format = opts.format;
|
|
194
|
+
spinner.text = `Rendering (${modified.canvas.width}\xD7${modified.canvas.height}, wait ${waitMs}ms)...`;
|
|
195
|
+
const result = await captureHtml({
|
|
196
|
+
html,
|
|
197
|
+
width: modified.canvas.width,
|
|
198
|
+
height: modified.canvas.height,
|
|
199
|
+
waitMs,
|
|
200
|
+
format,
|
|
201
|
+
quality: opts.quality,
|
|
202
|
+
scale: opts.scale
|
|
203
|
+
});
|
|
204
|
+
const outputPath = resolve(
|
|
205
|
+
opts.output ?? `${basename(filePath, extname(filePath))}.${format}`
|
|
206
|
+
);
|
|
207
|
+
await writeFile(outputPath, result.bytes);
|
|
208
|
+
spinner.succeed(
|
|
209
|
+
chalk.green(`Rendered ${modified.canvas.width}\xD7${modified.canvas.height} \u2192 ${outputPath}`)
|
|
210
|
+
);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
spinner.fail(chalk.red(`Render failed: ${err.message}`));
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
} finally {
|
|
215
|
+
await closeBrowser();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// src/commands/info.ts
|
|
220
|
+
import { Command as Command2 } from "commander";
|
|
221
|
+
import { resolve as resolve2 } from "path";
|
|
222
|
+
import chalk2 from "chalk";
|
|
223
|
+
function formatDate(iso) {
|
|
224
|
+
return iso.slice(0, 10);
|
|
225
|
+
}
|
|
226
|
+
function formatHumanReadable(filePath, sketch) {
|
|
227
|
+
const lines = [];
|
|
228
|
+
lines.push(chalk2.bold(filePath));
|
|
229
|
+
const pad = (label) => ` ${chalk2.dim(label.padEnd(14))}`;
|
|
230
|
+
lines.push(`${pad("Title:")}${sketch.title}`);
|
|
231
|
+
if (sketch.subtitle) lines.push(`${pad("Subtitle:")}${sketch.subtitle}`);
|
|
232
|
+
lines.push(`${pad("Renderer:")}${sketch.renderer.type}${sketch.renderer.version ? ` ${sketch.renderer.version}` : ""}`);
|
|
233
|
+
lines.push(`${pad("Canvas:")}${sketch.canvas.width}\xD7${sketch.canvas.height}${sketch.canvas.preset ? ` (${sketch.canvas.preset})` : ""}`);
|
|
234
|
+
lines.push(`${pad("Seed:")}${sketch.state.seed}`);
|
|
235
|
+
if (sketch.parameters.length > 0) {
|
|
236
|
+
const paramStr = sketch.parameters.map((p) => {
|
|
237
|
+
const val = sketch.state.params[p.key];
|
|
238
|
+
return `${p.key} (${val ?? p.default})`;
|
|
239
|
+
}).join(", ");
|
|
240
|
+
lines.push(`${pad("Parameters:")}${paramStr}`);
|
|
241
|
+
}
|
|
242
|
+
if (sketch.colors.length > 0) {
|
|
243
|
+
const palette = sketch.state.colorPalette;
|
|
244
|
+
const colorStr = Array.isArray(palette) && palette.length > 0 ? palette.join(", ") : sketch.colors.map((c) => c.default).join(", ");
|
|
245
|
+
lines.push(`${pad("Colors:")}${colorStr}`);
|
|
246
|
+
}
|
|
247
|
+
if (sketch.skills && sketch.skills.length > 0) {
|
|
248
|
+
lines.push(`${pad("Skills:")}${sketch.skills.join(", ")}`);
|
|
249
|
+
}
|
|
250
|
+
lines.push(`${pad("Created:")}${formatDate(sketch.created)}`);
|
|
251
|
+
lines.push(`${pad("Modified:")}${formatDate(sketch.modified)}`);
|
|
252
|
+
if (sketch.agent) lines.push(`${pad("Agent:")}${sketch.agent}`);
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
|
255
|
+
function formatTable(entries) {
|
|
256
|
+
const header = ["File", "Title", "Renderer", "Canvas", "Seed", "Params", "Colors"].join(" ");
|
|
257
|
+
const rows = entries.map(({ path: p, sketch }) => {
|
|
258
|
+
return [
|
|
259
|
+
p,
|
|
260
|
+
sketch.title,
|
|
261
|
+
sketch.renderer.type,
|
|
262
|
+
`${sketch.canvas.width}\xD7${sketch.canvas.height}`,
|
|
263
|
+
String(sketch.state.seed),
|
|
264
|
+
String(sketch.parameters.length),
|
|
265
|
+
String(sketch.colors.length)
|
|
266
|
+
].join(" ");
|
|
267
|
+
});
|
|
268
|
+
return [header, ...rows].join("\n");
|
|
269
|
+
}
|
|
270
|
+
var infoCommand = new Command2("info").description("Inspect .genart sketch metadata").argument("<files...>", "Path(s) to .genart file(s)").option("--json", "Machine-readable JSON output").option("--table", "Tabular output for multiple files").action(async (files, opts) => {
|
|
271
|
+
try {
|
|
272
|
+
const entries = [];
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
const filePath = resolve2(file);
|
|
275
|
+
const sketch = await loadSketch(filePath);
|
|
276
|
+
entries.push({ path: filePath, sketch });
|
|
277
|
+
}
|
|
278
|
+
if (opts.json) {
|
|
279
|
+
const data = entries.length === 1 ? entries[0].sketch : entries.map((e) => ({ file: e.path, ...e.sketch }));
|
|
280
|
+
console.log(JSON.stringify(data, null, 2));
|
|
281
|
+
} else if (opts.table) {
|
|
282
|
+
console.log(formatTable(entries));
|
|
283
|
+
} else {
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
console.log(formatHumanReadable(entry.path, entry.sketch));
|
|
286
|
+
if (entries.length > 1) console.log();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error(chalk2.red(`Error: ${err.message}`));
|
|
291
|
+
process.exitCode = 1;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// src/commands/validate.ts
|
|
296
|
+
import { Command as Command3 } from "commander";
|
|
297
|
+
import { resolve as resolve3 } from "path";
|
|
298
|
+
import { stat, readdir } from "fs/promises";
|
|
299
|
+
import chalk3 from "chalk";
|
|
300
|
+
import { createDefaultRegistry as createDefaultRegistry2 } from "@genart-dev/core";
|
|
301
|
+
async function resolveFiles(inputs) {
|
|
302
|
+
const files = [];
|
|
303
|
+
for (const input of inputs) {
|
|
304
|
+
const p = resolve3(input);
|
|
305
|
+
const info = await stat(p);
|
|
306
|
+
if (info.isDirectory()) {
|
|
307
|
+
const entries = await readdir(p);
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (entry.endsWith(".genart")) {
|
|
310
|
+
files.push(resolve3(p, entry));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
files.push(p);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return files;
|
|
318
|
+
}
|
|
319
|
+
var validateCommand = new Command3("validate").description("Validate .genart files").argument("<paths...>", "Path(s) to .genart file(s) or directories").option("--strict", "Also run adapter.validate() on algorithm source").action(async (paths, opts) => {
|
|
320
|
+
let hasErrors = false;
|
|
321
|
+
try {
|
|
322
|
+
const files = await resolveFiles(paths);
|
|
323
|
+
if (files.length === 0) {
|
|
324
|
+
console.error(chalk3.yellow("No .genart files found."));
|
|
325
|
+
process.exitCode = 1;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const registry = opts.strict ? createDefaultRegistry2() : null;
|
|
329
|
+
for (const filePath of files) {
|
|
330
|
+
try {
|
|
331
|
+
const sketch = await loadSketch(filePath);
|
|
332
|
+
if (opts.strict && registry) {
|
|
333
|
+
const adapter = registry.resolve(sketch.renderer.type);
|
|
334
|
+
const result = adapter.validate(sketch.algorithm);
|
|
335
|
+
if (!result.valid) {
|
|
336
|
+
console.error(chalk3.red(`\u2717 ${filePath}`));
|
|
337
|
+
for (const err of result.errors) {
|
|
338
|
+
console.error(chalk3.red(` ${err}`));
|
|
339
|
+
}
|
|
340
|
+
hasErrors = true;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
console.log(chalk3.green(`\u2713 ${filePath}`));
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.error(chalk3.red(`\u2717 ${filePath}`));
|
|
347
|
+
console.error(chalk3.red(` ${err.message}`));
|
|
348
|
+
hasErrors = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch (err) {
|
|
352
|
+
console.error(chalk3.red(`Error: ${err.message}`));
|
|
353
|
+
hasErrors = true;
|
|
354
|
+
}
|
|
355
|
+
if (hasErrors) {
|
|
356
|
+
process.exitCode = 1;
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// src/commands/init.ts
|
|
361
|
+
import { Command as Command4 } from "commander";
|
|
362
|
+
import { resolve as resolve4 } from "path";
|
|
363
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
364
|
+
import chalk4 from "chalk";
|
|
365
|
+
import ora2 from "ora";
|
|
366
|
+
import { createDefaultRegistry as createDefaultRegistry3 } from "@genart-dev/core";
|
|
367
|
+
import {
|
|
368
|
+
serializeGenart,
|
|
369
|
+
CANVAS_PRESETS,
|
|
370
|
+
resolvePreset as resolvePreset2
|
|
371
|
+
} from "@genart-dev/format";
|
|
372
|
+
var RENDERER_TYPES = ["p5", "canvas2d", "three", "glsl", "svg"];
|
|
373
|
+
function slugify(title) {
|
|
374
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
375
|
+
}
|
|
376
|
+
var initCommand = new Command4("init").description("Scaffold a new .genart sketch file").argument("[name]", "Sketch name / title").option("--renderer <type>", `Renderer type: ${RENDERER_TYPES.join(", ")}`).option("--preset <name>", "Canvas preset", "square-600").option("--title <string>", "Sketch title").action(async (name, opts) => {
|
|
377
|
+
const spinner = ora2("").start();
|
|
378
|
+
spinner.stop();
|
|
379
|
+
try {
|
|
380
|
+
let rendererType;
|
|
381
|
+
let preset;
|
|
382
|
+
let title;
|
|
383
|
+
if (opts.renderer) {
|
|
384
|
+
rendererType = opts.renderer;
|
|
385
|
+
if (!RENDERER_TYPES.includes(rendererType)) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Invalid renderer "${rendererType}". Choose from: ${RENDERER_TYPES.join(", ")}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
const { default: inquirer } = await import("inquirer");
|
|
392
|
+
const answers = await inquirer.prompt([
|
|
393
|
+
{
|
|
394
|
+
type: "list",
|
|
395
|
+
name: "renderer",
|
|
396
|
+
message: "Renderer type:",
|
|
397
|
+
choices: RENDERER_TYPES.map((t) => ({ name: t, value: t }))
|
|
398
|
+
}
|
|
399
|
+
]);
|
|
400
|
+
rendererType = answers.renderer;
|
|
401
|
+
}
|
|
402
|
+
if (opts.title) {
|
|
403
|
+
title = opts.title;
|
|
404
|
+
} else if (name) {
|
|
405
|
+
title = name;
|
|
406
|
+
} else {
|
|
407
|
+
const { default: inquirer } = await import("inquirer");
|
|
408
|
+
const answers = await inquirer.prompt([
|
|
409
|
+
{
|
|
410
|
+
type: "input",
|
|
411
|
+
name: "title",
|
|
412
|
+
message: "Sketch title:",
|
|
413
|
+
default: "Untitled Sketch"
|
|
414
|
+
}
|
|
415
|
+
]);
|
|
416
|
+
title = answers.title;
|
|
417
|
+
}
|
|
418
|
+
if (opts.preset) {
|
|
419
|
+
preset = opts.preset;
|
|
420
|
+
} else {
|
|
421
|
+
const { default: inquirer } = await import("inquirer");
|
|
422
|
+
const choices = CANVAS_PRESETS.map((p) => ({
|
|
423
|
+
name: `${p.id} (${p.width}\xD7${p.height})`,
|
|
424
|
+
value: p.id
|
|
425
|
+
}));
|
|
426
|
+
const answers = await inquirer.prompt([
|
|
427
|
+
{
|
|
428
|
+
type: "list",
|
|
429
|
+
name: "preset",
|
|
430
|
+
message: "Canvas preset:",
|
|
431
|
+
choices,
|
|
432
|
+
default: "square-600"
|
|
433
|
+
}
|
|
434
|
+
]);
|
|
435
|
+
preset = answers.preset;
|
|
436
|
+
}
|
|
437
|
+
const dims = resolvePreset2(preset);
|
|
438
|
+
const registry = createDefaultRegistry3();
|
|
439
|
+
const adapter = registry.resolve(rendererType);
|
|
440
|
+
const algorithm = adapter.getAlgorithmTemplate();
|
|
441
|
+
const id = slugify(title);
|
|
442
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
443
|
+
const sketch = {
|
|
444
|
+
genart: "1.0",
|
|
445
|
+
id,
|
|
446
|
+
title,
|
|
447
|
+
created: now,
|
|
448
|
+
modified: now,
|
|
449
|
+
renderer: { type: rendererType },
|
|
450
|
+
canvas: { preset, width: dims.width, height: dims.height },
|
|
451
|
+
parameters: [],
|
|
452
|
+
colors: [],
|
|
453
|
+
state: {
|
|
454
|
+
seed: Math.floor(Math.random() * 1e4),
|
|
455
|
+
params: {},
|
|
456
|
+
colorPalette: []
|
|
457
|
+
},
|
|
458
|
+
algorithm
|
|
459
|
+
};
|
|
460
|
+
const outputPath = resolve4(`${id}.genart`);
|
|
461
|
+
const json = serializeGenart(sketch);
|
|
462
|
+
await writeFile2(outputPath, json, "utf-8");
|
|
463
|
+
console.log(chalk4.green(`\u2713 Created ${outputPath}`));
|
|
464
|
+
console.log(chalk4.dim(` Renderer: ${rendererType}`));
|
|
465
|
+
console.log(chalk4.dim(` Canvas: ${dims.width}\xD7${dims.height} (${preset})`));
|
|
466
|
+
} catch (err) {
|
|
467
|
+
console.error(chalk4.red(`Error: ${err.message}`));
|
|
468
|
+
process.exitCode = 1;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// src/commands/export.ts
|
|
473
|
+
import { Command as Command5 } from "commander";
|
|
474
|
+
import { resolve as resolve5, basename as basename2, extname as extname2 } from "path";
|
|
475
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
476
|
+
import chalk5 from "chalk";
|
|
477
|
+
import ora3 from "ora";
|
|
478
|
+
import { createDefaultRegistry as createDefaultRegistry4 } from "@genart-dev/core";
|
|
479
|
+
var exportCommand = new Command5("export").description("Export sketch as HTML, image, or algorithm source").argument("<file>", "Path to .genart file").option("--format <fmt>", "Export format: html, png, jpeg, webp, algorithm", "html").option("--wait <duration>", "Render wait time (for image formats)", "500ms").option("--seed <n>", "Override seed", Number).option("--params <json>", "Override parameters (JSON object)").option("--colors <json>", "Override color palette (JSON array)").option("--width <n>", "Override canvas width", Number).option("--height <n>", "Override canvas height", Number).option("--preset <name>", "Use a canvas preset").option("--quality <n>", "Lossy compression quality (0-100)", Number, 80).option("--scale <n>", "Pixel density multiplier", Number, 1).option("-o, --output <path>", "Output file path").action(async (file, opts) => {
|
|
480
|
+
const spinner = ora3("Loading sketch...").start();
|
|
481
|
+
try {
|
|
482
|
+
const filePath = resolve5(file);
|
|
483
|
+
const sketch = await loadSketch(filePath);
|
|
484
|
+
const overrides = {};
|
|
485
|
+
if (opts.seed !== void 0) overrides.seed = opts.seed;
|
|
486
|
+
if (opts.width !== void 0) overrides.width = opts.width;
|
|
487
|
+
if (opts.height !== void 0) overrides.height = opts.height;
|
|
488
|
+
if (opts.preset) overrides.preset = opts.preset;
|
|
489
|
+
if (opts.params) {
|
|
490
|
+
overrides.params = JSON.parse(opts.params);
|
|
491
|
+
}
|
|
492
|
+
if (opts.colors) {
|
|
493
|
+
overrides.colors = JSON.parse(opts.colors);
|
|
494
|
+
}
|
|
495
|
+
const modified = applyOverrides(sketch, overrides);
|
|
496
|
+
const registry = createDefaultRegistry4();
|
|
497
|
+
const adapter = registry.resolve(modified.renderer.type);
|
|
498
|
+
const format = opts.format;
|
|
499
|
+
const baseName = basename2(filePath, extname2(filePath));
|
|
500
|
+
let outputPath;
|
|
501
|
+
let content;
|
|
502
|
+
if (format === "html") {
|
|
503
|
+
spinner.text = "Generating standalone HTML...";
|
|
504
|
+
content = adapter.generateStandaloneHTML(modified);
|
|
505
|
+
outputPath = resolve5(opts.output ?? `${baseName}.html`);
|
|
506
|
+
} else if (format === "algorithm") {
|
|
507
|
+
spinner.text = "Extracting algorithm...";
|
|
508
|
+
content = modified.algorithm;
|
|
509
|
+
const ext = modified.renderer.type === "glsl" ? "glsl" : "js";
|
|
510
|
+
outputPath = resolve5(opts.output ?? `${baseName}.${ext}`);
|
|
511
|
+
} else if (format === "png" || format === "jpeg" || format === "webp") {
|
|
512
|
+
spinner.text = `Rendering ${format}...`;
|
|
513
|
+
const html = adapter.generateStandaloneHTML(modified);
|
|
514
|
+
const waitMs = parseWait(opts.wait);
|
|
515
|
+
const result = await captureHtml({
|
|
516
|
+
html,
|
|
517
|
+
width: modified.canvas.width,
|
|
518
|
+
height: modified.canvas.height,
|
|
519
|
+
waitMs,
|
|
520
|
+
format,
|
|
521
|
+
quality: opts.quality,
|
|
522
|
+
scale: opts.scale
|
|
523
|
+
});
|
|
524
|
+
content = result.bytes;
|
|
525
|
+
outputPath = resolve5(opts.output ?? `${baseName}.${format}`);
|
|
526
|
+
} else {
|
|
527
|
+
throw new Error(`Unsupported export format: "${format}". Use: html, png, jpeg, webp, algorithm`);
|
|
528
|
+
}
|
|
529
|
+
await writeFile3(outputPath, content, typeof content === "string" ? "utf-8" : void 0);
|
|
530
|
+
spinner.succeed(chalk5.green(`Exported \u2192 ${outputPath}`));
|
|
531
|
+
} catch (err) {
|
|
532
|
+
spinner.fail(chalk5.red(`Export failed: ${err.message}`));
|
|
533
|
+
process.exitCode = 1;
|
|
534
|
+
} finally {
|
|
535
|
+
await closeBrowser();
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// src/commands/batch.ts
|
|
540
|
+
import { Command as Command6 } from "commander";
|
|
541
|
+
import { resolve as resolve6, basename as basename3, extname as extname3 } from "path";
|
|
542
|
+
import { writeFile as writeFile4, mkdir } from "fs/promises";
|
|
543
|
+
import chalk6 from "chalk";
|
|
544
|
+
import ora4 from "ora";
|
|
545
|
+
import { createDefaultRegistry as createDefaultRegistry5 } from "@genart-dev/core";
|
|
546
|
+
|
|
547
|
+
// src/util/parse-seeds.ts
|
|
548
|
+
function parseSeeds(value) {
|
|
549
|
+
const seeds = [];
|
|
550
|
+
for (const part of value.split(",")) {
|
|
551
|
+
const trimmed = part.trim();
|
|
552
|
+
if (trimmed.includes("-")) {
|
|
553
|
+
const [startStr, endStr] = trimmed.split("-");
|
|
554
|
+
const start = Number(startStr);
|
|
555
|
+
const end = Number(endStr);
|
|
556
|
+
if (Number.isNaN(start) || Number.isNaN(end) || start > end) {
|
|
557
|
+
throw new Error(`Invalid seed range: "${trimmed}". Use "1-100".`);
|
|
558
|
+
}
|
|
559
|
+
for (let i = start; i <= end; i++) {
|
|
560
|
+
seeds.push(i);
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
const n = Number(trimmed);
|
|
564
|
+
if (Number.isNaN(n) || !Number.isInteger(n)) {
|
|
565
|
+
throw new Error(`Invalid seed value: "${trimmed}". Must be an integer.`);
|
|
566
|
+
}
|
|
567
|
+
seeds.push(n);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (seeds.length === 0) {
|
|
571
|
+
throw new Error(`No seeds parsed from: "${value}"`);
|
|
572
|
+
}
|
|
573
|
+
return seeds;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/util/parse-sweep.ts
|
|
577
|
+
function parseSweep(value) {
|
|
578
|
+
const match = value.match(/^([^=]+)=([^:]+):([^:]+):(.+)$/);
|
|
579
|
+
if (!match) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
`Invalid sweep format: "${value}". Use "param=min:max:step" (e.g. "amplitude=0:1:0.1").`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
const [, key, minStr, maxStr, stepStr] = match;
|
|
585
|
+
const min = Number(minStr);
|
|
586
|
+
const max = Number(maxStr);
|
|
587
|
+
const step = Number(stepStr);
|
|
588
|
+
if (Number.isNaN(min) || Number.isNaN(max) || Number.isNaN(step)) {
|
|
589
|
+
throw new Error(`Invalid numeric values in sweep: "${value}".`);
|
|
590
|
+
}
|
|
591
|
+
if (step <= 0) {
|
|
592
|
+
throw new Error(`Sweep step must be positive: "${value}".`);
|
|
593
|
+
}
|
|
594
|
+
if (min > max) {
|
|
595
|
+
throw new Error(`Sweep min must be \u2264 max: "${value}".`);
|
|
596
|
+
}
|
|
597
|
+
const values = [];
|
|
598
|
+
for (let v = min; v <= max + step * 1e-3; v += step) {
|
|
599
|
+
values.push(Math.round(v * 1e10) / 1e10);
|
|
600
|
+
}
|
|
601
|
+
if (values.length > 0 && values[values.length - 1] > max) {
|
|
602
|
+
values.pop();
|
|
603
|
+
}
|
|
604
|
+
return { key, min, max, step, values };
|
|
605
|
+
}
|
|
606
|
+
function cartesianProduct(seeds, sweeps) {
|
|
607
|
+
if (sweeps.length === 0) {
|
|
608
|
+
return seeds.map((seed) => ({ seed, params: {} }));
|
|
609
|
+
}
|
|
610
|
+
const paramCombos = sweepProduct(sweeps);
|
|
611
|
+
const results = [];
|
|
612
|
+
for (const seed of seeds) {
|
|
613
|
+
for (const params of paramCombos) {
|
|
614
|
+
results.push({ seed, params });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return results;
|
|
618
|
+
}
|
|
619
|
+
function sweepProduct(sweeps) {
|
|
620
|
+
if (sweeps.length === 0) return [{}];
|
|
621
|
+
const [first, ...rest] = sweeps;
|
|
622
|
+
const restCombos = sweepProduct(rest);
|
|
623
|
+
const results = [];
|
|
624
|
+
for (const value of first.values) {
|
|
625
|
+
for (const combo of restCombos) {
|
|
626
|
+
results.push({ [first.key]: value, ...combo });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return results;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/util/naming.ts
|
|
633
|
+
function formatOutputName(pattern, ctx) {
|
|
634
|
+
let result = pattern;
|
|
635
|
+
result = result.replace(/\{id\}/g, ctx.id);
|
|
636
|
+
result = result.replace(/\{seed\}/g, String(ctx.seed));
|
|
637
|
+
result = result.replace(/\{index\}/g, String(ctx.index).padStart(4, "0"));
|
|
638
|
+
if (result.includes("{params}")) {
|
|
639
|
+
const paramStr = Object.entries(ctx.params).map(([k, v]) => `${k}=${v}`).join("_");
|
|
640
|
+
result = result.replace(/\{params\}/g, paramStr || "default");
|
|
641
|
+
}
|
|
642
|
+
return `${result}.${ctx.format}`;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/commands/batch.ts
|
|
646
|
+
function collectSweeps(value, prev) {
|
|
647
|
+
return [...prev, value];
|
|
648
|
+
}
|
|
649
|
+
var batchCommand = new Command6("batch").description("Generate many renders from one sketch \u2014 seed ranges, parameter sweeps").argument("<files...>", "Path(s) to .genart file(s)").option("--seeds <range>", "Seed range or list (e.g. 1-100, 1,5,42)").option("--sweep <spec>", "Parameter sweep (repeatable, e.g. amplitude=0:1:0.1)", collectSweeps, []).option("--random <n>", "Generate N random seed + param combinations", Number).option("--matrix", "Cartesian product of seeds \xD7 sweeps").option("--concurrency <n>", "Parallel captures", Number, 4).option("--naming <pattern>", "Output naming pattern: {id}, {seed}, {index}, {params}", "{id}-{seed}").option("--manifest", "Write manifest.json with per-render metadata").option("--wait <duration>", "Render wait time", "500ms").option("--width <n>", "Override canvas width", Number).option("--height <n>", "Override canvas height", Number).option("--preset <name>", "Use a canvas preset").option("--format <fmt>", "Output format: png, jpeg, webp", "png").option("--quality <n>", "Lossy compression quality (0-100)", Number, 80).option("--scale <n>", "Pixel density multiplier", Number, 1).option("--colors <json>", "Override color palette (JSON array)").option("-o, --output-dir <dir>", "Output directory", ".").action(async (files, opts) => {
|
|
650
|
+
const spinner = ora4("Preparing batch...").start();
|
|
651
|
+
try {
|
|
652
|
+
const outputDir = resolve6(opts.outputDir);
|
|
653
|
+
await mkdir(outputDir, { recursive: true });
|
|
654
|
+
const waitMs = parseWait(opts.wait);
|
|
655
|
+
const format = opts.format;
|
|
656
|
+
const concurrency = opts.concurrency;
|
|
657
|
+
const namingPattern = opts.naming;
|
|
658
|
+
const registry = createDefaultRegistry5();
|
|
659
|
+
const manifest = [];
|
|
660
|
+
let totalRendered = 0;
|
|
661
|
+
for (const file of files) {
|
|
662
|
+
const filePath = resolve6(file);
|
|
663
|
+
const sketch = await loadSketch(filePath);
|
|
664
|
+
const sketchId = sketch.id ?? basename3(filePath, extname3(filePath));
|
|
665
|
+
const baseOverrides = {};
|
|
666
|
+
if (opts.width !== void 0) baseOverrides.width = opts.width;
|
|
667
|
+
if (opts.height !== void 0) baseOverrides.height = opts.height;
|
|
668
|
+
if (opts.preset) baseOverrides.preset = opts.preset;
|
|
669
|
+
if (opts.colors) {
|
|
670
|
+
baseOverrides.colors = JSON.parse(opts.colors);
|
|
671
|
+
}
|
|
672
|
+
const jobs = generateJobs(opts, sketch.state.seed);
|
|
673
|
+
spinner.text = `Batch: ${jobs.length} render${jobs.length === 1 ? "" : "s"} for ${basename3(filePath)}`;
|
|
674
|
+
for (let i = 0; i < jobs.length; i += concurrency) {
|
|
675
|
+
const chunk = jobs.slice(i, i + concurrency);
|
|
676
|
+
const promises = chunk.map(async (job, chunkIdx) => {
|
|
677
|
+
const idx = i + chunkIdx;
|
|
678
|
+
const overrides = {
|
|
679
|
+
...baseOverrides,
|
|
680
|
+
seed: job.seed,
|
|
681
|
+
...Object.keys(job.params).length > 0 ? { params: job.params } : {}
|
|
682
|
+
};
|
|
683
|
+
const modified = applyOverrides(sketch, overrides);
|
|
684
|
+
const adapter = registry.resolve(modified.renderer.type);
|
|
685
|
+
const html = adapter.generateStandaloneHTML(modified);
|
|
686
|
+
const result = await captureHtml({
|
|
687
|
+
html,
|
|
688
|
+
width: modified.canvas.width,
|
|
689
|
+
height: modified.canvas.height,
|
|
690
|
+
waitMs,
|
|
691
|
+
format,
|
|
692
|
+
quality: opts.quality,
|
|
693
|
+
scale: opts.scale
|
|
694
|
+
});
|
|
695
|
+
const fileName = formatOutputName(namingPattern, {
|
|
696
|
+
id: sketchId,
|
|
697
|
+
seed: job.seed,
|
|
698
|
+
index: idx,
|
|
699
|
+
params: job.params,
|
|
700
|
+
format
|
|
701
|
+
});
|
|
702
|
+
const outputPath = resolve6(outputDir, fileName);
|
|
703
|
+
await writeFile4(outputPath, result.bytes);
|
|
704
|
+
manifest.push({
|
|
705
|
+
file: basename3(filePath),
|
|
706
|
+
seed: job.seed,
|
|
707
|
+
params: job.params,
|
|
708
|
+
path: outputPath,
|
|
709
|
+
width: modified.canvas.width,
|
|
710
|
+
height: modified.canvas.height,
|
|
711
|
+
format
|
|
712
|
+
});
|
|
713
|
+
return outputPath;
|
|
714
|
+
});
|
|
715
|
+
await Promise.all(promises);
|
|
716
|
+
totalRendered += chunk.length;
|
|
717
|
+
spinner.text = `Batch: ${totalRendered}/${jobs.length} rendered`;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (opts.manifest) {
|
|
721
|
+
const manifestPath = resolve6(outputDir, "manifest.json");
|
|
722
|
+
await writeFile4(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
723
|
+
console.log(JSON.stringify(manifest));
|
|
724
|
+
}
|
|
725
|
+
spinner.succeed(
|
|
726
|
+
chalk6.green(`Batch complete: ${totalRendered} render${totalRendered === 1 ? "" : "s"}`)
|
|
727
|
+
);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
spinner.fail(chalk6.red(`Batch failed: ${err.message}`));
|
|
730
|
+
process.exitCode = 1;
|
|
731
|
+
} finally {
|
|
732
|
+
await closeBrowser();
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
function generateJobs(opts, defaultSeed) {
|
|
736
|
+
const seeds = opts.seeds ? parseSeeds(opts.seeds) : [defaultSeed];
|
|
737
|
+
const sweeps = opts.sweep.map(parseSweep);
|
|
738
|
+
if (opts.random) {
|
|
739
|
+
const n = opts.random;
|
|
740
|
+
const jobs = [];
|
|
741
|
+
for (let i = 0; i < n; i++) {
|
|
742
|
+
const seed = Math.floor(Math.random() * 1e5);
|
|
743
|
+
const params = {};
|
|
744
|
+
for (const sweep of sweeps) {
|
|
745
|
+
params[sweep.key] = sweep.min + Math.random() * (sweep.max - sweep.min);
|
|
746
|
+
params[sweep.key] = Math.round(params[sweep.key] / sweep.step) * sweep.step;
|
|
747
|
+
params[sweep.key] = Math.round(params[sweep.key] * 1e10) / 1e10;
|
|
748
|
+
}
|
|
749
|
+
jobs.push({ seed, params });
|
|
750
|
+
}
|
|
751
|
+
return jobs;
|
|
752
|
+
}
|
|
753
|
+
if (opts.matrix || sweeps.length > 0) {
|
|
754
|
+
return cartesianProduct(seeds, sweeps);
|
|
755
|
+
}
|
|
756
|
+
return seeds.map((seed) => ({ seed, params: {} }));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/commands/montage.ts
|
|
760
|
+
import { Command as Command7 } from "commander";
|
|
761
|
+
import { resolve as resolve7, basename as basename4 } from "path";
|
|
762
|
+
import { readFile as readFile2, readdir as readdir2, writeFile as writeFile5, stat as stat2 } from "fs/promises";
|
|
763
|
+
import chalk7 from "chalk";
|
|
764
|
+
import ora5 from "ora";
|
|
765
|
+
var montageCommand = new Command7("montage").description("Compose a grid of images into a single montage").argument("<source>", "Directory of images, or - for manifest JSON on stdin").option("--columns <n>", "Grid columns", Number).option("--rows <n>", "Grid rows", Number).option("--tile-size <WxH>", "Force tile dimensions (e.g. 200x200)").option("--gap <px>", "Gap between tiles", Number, 2).option("--padding <px>", "Outer padding", Number, 0).option("--background <hex>", "Background color", "#0A0A0A").option("--label <mode>", "Label tiles: seed, params, filename, index, none", "none").option("--label-color <hex>", "Label text color", "#999999").option("--label-font-size <px>", "Label font size", Number, 11).option("--sort <key>", "Sort: seed, name, param:<key>").option("-o, --output <path>", "Output file", "montage.png").action(async (source, opts) => {
|
|
766
|
+
const spinner = ora5("Preparing montage...").start();
|
|
767
|
+
try {
|
|
768
|
+
let sharp;
|
|
769
|
+
try {
|
|
770
|
+
sharp = (await import("sharp")).default;
|
|
771
|
+
} catch {
|
|
772
|
+
throw new Error(
|
|
773
|
+
`sharp is required for montage composition but is not installed.
|
|
774
|
+
Install it: npm install sharp
|
|
775
|
+
Or: pnpm add sharp`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
let entries;
|
|
779
|
+
if (source === "-") {
|
|
780
|
+
const input = await readStdin();
|
|
781
|
+
const manifest = JSON.parse(input);
|
|
782
|
+
entries = manifest.map((m, i) => ({
|
|
783
|
+
path: m.path,
|
|
784
|
+
seed: m.seed,
|
|
785
|
+
params: m.params,
|
|
786
|
+
index: i
|
|
787
|
+
}));
|
|
788
|
+
} else {
|
|
789
|
+
const dir = resolve7(source);
|
|
790
|
+
const dirStat = await stat2(dir);
|
|
791
|
+
if (!dirStat.isDirectory()) {
|
|
792
|
+
throw new Error(`Not a directory: ${dir}`);
|
|
793
|
+
}
|
|
794
|
+
const files = await readdir2(dir);
|
|
795
|
+
const imageFiles = files.filter((f) => /\.(png|jpe?g|webp)$/i.test(f)).sort();
|
|
796
|
+
entries = imageFiles.map((f, i) => ({
|
|
797
|
+
path: resolve7(dir, f),
|
|
798
|
+
index: i
|
|
799
|
+
}));
|
|
800
|
+
}
|
|
801
|
+
if (entries.length === 0) {
|
|
802
|
+
throw new Error("No images found for montage.");
|
|
803
|
+
}
|
|
804
|
+
sortEntries(entries, opts.sort);
|
|
805
|
+
spinner.text = `Loading ${entries.length} image${entries.length === 1 ? "" : "s"}...`;
|
|
806
|
+
const images = await Promise.all(
|
|
807
|
+
entries.map(async (entry) => {
|
|
808
|
+
const buffer = await readFile2(entry.path);
|
|
809
|
+
const metadata = await sharp(buffer).metadata();
|
|
810
|
+
return {
|
|
811
|
+
entry,
|
|
812
|
+
buffer,
|
|
813
|
+
width: metadata.width,
|
|
814
|
+
height: metadata.height
|
|
815
|
+
};
|
|
816
|
+
})
|
|
817
|
+
);
|
|
818
|
+
let tileWidth;
|
|
819
|
+
let tileHeight;
|
|
820
|
+
if (opts.tileSize) {
|
|
821
|
+
const [w, h] = opts.tileSize.split("x").map(Number);
|
|
822
|
+
tileWidth = w;
|
|
823
|
+
tileHeight = h;
|
|
824
|
+
} else {
|
|
825
|
+
tileWidth = images[0].width;
|
|
826
|
+
tileHeight = images[0].height;
|
|
827
|
+
}
|
|
828
|
+
const count = images.length;
|
|
829
|
+
let columns;
|
|
830
|
+
let rows;
|
|
831
|
+
if (opts.columns) {
|
|
832
|
+
columns = opts.columns;
|
|
833
|
+
rows = Math.ceil(count / columns);
|
|
834
|
+
} else if (opts.rows) {
|
|
835
|
+
rows = opts.rows;
|
|
836
|
+
columns = Math.ceil(count / rows);
|
|
837
|
+
} else {
|
|
838
|
+
columns = Math.ceil(Math.sqrt(count));
|
|
839
|
+
rows = Math.ceil(count / columns);
|
|
840
|
+
}
|
|
841
|
+
const gap = opts.gap;
|
|
842
|
+
const padding = opts.padding;
|
|
843
|
+
const background = opts.background;
|
|
844
|
+
const totalWidth = padding * 2 + columns * tileWidth + (columns - 1) * gap;
|
|
845
|
+
const totalHeight = padding * 2 + rows * tileHeight + (rows - 1) * gap;
|
|
846
|
+
spinner.text = `Composing ${columns}\xD7${rows} grid (${totalWidth}\xD7${totalHeight})...`;
|
|
847
|
+
const composites = [];
|
|
848
|
+
for (let i = 0; i < images.length; i++) {
|
|
849
|
+
const col = i % columns;
|
|
850
|
+
const row = Math.floor(i / columns);
|
|
851
|
+
const left = padding + col * (tileWidth + gap);
|
|
852
|
+
const top = padding + row * (tileHeight + gap);
|
|
853
|
+
let tileBuffer;
|
|
854
|
+
if (images[i].width !== tileWidth || images[i].height !== tileHeight) {
|
|
855
|
+
tileBuffer = await sharp(images[i].buffer).resize(tileWidth, tileHeight, { fit: "cover" }).toBuffer();
|
|
856
|
+
} else {
|
|
857
|
+
tileBuffer = Buffer.from(images[i].buffer);
|
|
858
|
+
}
|
|
859
|
+
composites.push({ input: tileBuffer, left, top });
|
|
860
|
+
}
|
|
861
|
+
const montageBuffer = await sharp({
|
|
862
|
+
create: {
|
|
863
|
+
width: totalWidth,
|
|
864
|
+
height: totalHeight,
|
|
865
|
+
channels: 4,
|
|
866
|
+
background: hexToRgba(background)
|
|
867
|
+
}
|
|
868
|
+
}).composite(composites).png().toBuffer();
|
|
869
|
+
const outputPath = resolve7(opts.output);
|
|
870
|
+
await writeFile5(outputPath, montageBuffer);
|
|
871
|
+
spinner.succeed(
|
|
872
|
+
chalk7.green(
|
|
873
|
+
`Montage: ${count} tile${count === 1 ? "" : "s"} \u2192 ${columns}\xD7${rows} grid (${totalWidth}\xD7${totalHeight}) \u2192 ${outputPath}`
|
|
874
|
+
)
|
|
875
|
+
);
|
|
876
|
+
} catch (err) {
|
|
877
|
+
spinner.fail(chalk7.red(`Montage failed: ${err.message}`));
|
|
878
|
+
process.exitCode = 1;
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
function readStdin() {
|
|
882
|
+
return new Promise((resolve10, reject) => {
|
|
883
|
+
let data = "";
|
|
884
|
+
process.stdin.setEncoding("utf-8");
|
|
885
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
886
|
+
process.stdin.on("end", () => resolve10(data));
|
|
887
|
+
process.stdin.on("error", reject);
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
function sortEntries(entries, sortKey) {
|
|
891
|
+
if (!sortKey) return;
|
|
892
|
+
if (sortKey === "seed") {
|
|
893
|
+
entries.sort((a, b) => (a.seed ?? 0) - (b.seed ?? 0));
|
|
894
|
+
} else if (sortKey === "name") {
|
|
895
|
+
entries.sort((a, b) => basename4(a.path).localeCompare(basename4(b.path)));
|
|
896
|
+
} else if (sortKey.startsWith("param:")) {
|
|
897
|
+
const paramKey = sortKey.slice(6);
|
|
898
|
+
entries.sort((a, b) => {
|
|
899
|
+
const va = a.params?.[paramKey] ?? 0;
|
|
900
|
+
const vb = b.params?.[paramKey] ?? 0;
|
|
901
|
+
return va - vb;
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function hexToRgba(hex) {
|
|
906
|
+
const h = hex.replace("#", "");
|
|
907
|
+
return {
|
|
908
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
909
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
910
|
+
b: parseInt(h.slice(4, 6), 16),
|
|
911
|
+
alpha: 1
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/commands/import.ts
|
|
916
|
+
import { Command as Command8 } from "commander";
|
|
917
|
+
import { resolve as resolve8, basename as basename5, extname as extname4 } from "path";
|
|
918
|
+
import { readFile as readFile3, writeFile as writeFile6 } from "fs/promises";
|
|
919
|
+
import chalk8 from "chalk";
|
|
920
|
+
import ora6 from "ora";
|
|
921
|
+
import { createDefaultRegistry as createDefaultRegistry6 } from "@genart-dev/core";
|
|
922
|
+
import {
|
|
923
|
+
serializeGenart as serializeGenart2,
|
|
924
|
+
resolvePreset as resolvePreset3
|
|
925
|
+
} from "@genart-dev/format";
|
|
926
|
+
|
|
927
|
+
// src/detect/renderer.ts
|
|
928
|
+
function detectRenderer(source) {
|
|
929
|
+
const results = [];
|
|
930
|
+
const glslSignals = [];
|
|
931
|
+
if (/^\s*#version\s+/m.test(source)) glslSignals.push("#version directive");
|
|
932
|
+
if (/void\s+main\s*\(\s*\)/.test(source)) glslSignals.push("void main()");
|
|
933
|
+
if (/gl_Frag(Color|Coord)/i.test(source)) glslSignals.push("gl_Frag* builtins");
|
|
934
|
+
if (/\b(uniform|varying|attribute)\s+/m.test(source)) glslSignals.push("GLSL qualifiers");
|
|
935
|
+
if (/\b(vec[234]|mat[234]|sampler2D)\b/.test(source)) glslSignals.push("GLSL types");
|
|
936
|
+
if (glslSignals.length >= 2) {
|
|
937
|
+
results.push({
|
|
938
|
+
type: "glsl",
|
|
939
|
+
confidence: glslSignals.length >= 3 ? "high" : "medium",
|
|
940
|
+
signals: glslSignals,
|
|
941
|
+
score: glslSignals.length * 10
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
const p5Signals = [];
|
|
945
|
+
if (/function\s+sketch\s*\(\s*p\s*[,)]/.test(source)) p5Signals.push("sketch(p, ...) signature");
|
|
946
|
+
if (/p\.(setup|draw|createCanvas)\b/.test(source)) p5Signals.push("p5 instance methods");
|
|
947
|
+
if (/p\.(background|fill|stroke|ellipse|rect|line|vertex|bezier)\b/.test(source)) p5Signals.push("p5 drawing API");
|
|
948
|
+
if (/p\.(random|noise|map|lerp|constrain)\b/.test(source)) p5Signals.push("p5 math utilities");
|
|
949
|
+
if (/p\.(push|pop|translate|rotate|scale)\b/.test(source)) p5Signals.push("p5 transforms");
|
|
950
|
+
if (/p\.randomSeed\b/.test(source)) p5Signals.push("p5 randomSeed");
|
|
951
|
+
if (p5Signals.length >= 2) {
|
|
952
|
+
results.push({
|
|
953
|
+
type: "p5",
|
|
954
|
+
confidence: p5Signals.length >= 3 ? "high" : "medium",
|
|
955
|
+
signals: p5Signals,
|
|
956
|
+
score: p5Signals.length * 10
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
const canvas2dSignals = [];
|
|
960
|
+
if (/function\s+sketch\s*\(\s*ctx\s*[,)]/.test(source)) canvas2dSignals.push("sketch(ctx, ...) signature");
|
|
961
|
+
if (/ctx\.(fillRect|strokeRect|clearRect)\b/.test(source)) canvas2dSignals.push("Canvas2D rect methods");
|
|
962
|
+
if (/ctx\.(beginPath|moveTo|lineTo|arc|closePath)\b/.test(source)) canvas2dSignals.push("Canvas2D path API");
|
|
963
|
+
if (/ctx\.(fillStyle|strokeStyle|lineWidth|globalAlpha)\b/.test(source)) canvas2dSignals.push("Canvas2D style props");
|
|
964
|
+
if (/ctx\.(save|restore|translate|rotate|scale)\b/.test(source)) canvas2dSignals.push("Canvas2D transforms");
|
|
965
|
+
if (canvas2dSignals.length >= 2) {
|
|
966
|
+
results.push({
|
|
967
|
+
type: "canvas2d",
|
|
968
|
+
confidence: canvas2dSignals.length >= 3 ? "high" : "medium",
|
|
969
|
+
signals: canvas2dSignals,
|
|
970
|
+
score: canvas2dSignals.length * 10
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
const threeSignals = [];
|
|
974
|
+
if (/\bTHREE\b/.test(source)) threeSignals.push("THREE namespace");
|
|
975
|
+
if (/new\s+THREE\.\w+/.test(source)) threeSignals.push("THREE constructor calls");
|
|
976
|
+
if (/THREE\.(Scene|PerspectiveCamera|WebGLRenderer|Mesh|BoxGeometry|SphereGeometry)\b/.test(source)) threeSignals.push("THREE core classes");
|
|
977
|
+
if (/THREE\.(MeshBasicMaterial|MeshStandardMaterial|ShaderMaterial)\b/.test(source)) threeSignals.push("THREE materials");
|
|
978
|
+
if (/THREE\.(Vector[23]|Color|Euler)\b/.test(source)) threeSignals.push("THREE math types");
|
|
979
|
+
if (threeSignals.length >= 2) {
|
|
980
|
+
results.push({
|
|
981
|
+
type: "three",
|
|
982
|
+
confidence: threeSignals.length >= 3 ? "high" : "medium",
|
|
983
|
+
signals: threeSignals,
|
|
984
|
+
score: threeSignals.length * 10
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
const svgSignals = [];
|
|
988
|
+
if (/document\.createElementNS\s*\(\s*["']http:\/\/www\.w3\.org\/2000\/svg["']/.test(source)) svgSignals.push("SVG namespace createElement");
|
|
989
|
+
if (/\.(setAttribute|getAttribute)\s*\(\s*["'](d|viewBox|fill|stroke|transform)["']/.test(source)) svgSignals.push("SVG attribute methods");
|
|
990
|
+
if (/\b(path|circle|rect|line|polygon|polyline|ellipse|g|svg)\b/.test(source) && /createElementNS|innerHTML/.test(source)) svgSignals.push("SVG element names");
|
|
991
|
+
if (/\bM\s*[\d.-]+[\s,]+[\d.-]+.*[LQCZ]/i.test(source)) svgSignals.push("SVG path data");
|
|
992
|
+
if (svgSignals.length >= 1) {
|
|
993
|
+
results.push({
|
|
994
|
+
type: "svg",
|
|
995
|
+
confidence: svgSignals.length >= 2 ? "high" : svgSignals.length >= 1 ? "medium" : "low",
|
|
996
|
+
signals: svgSignals,
|
|
997
|
+
score: svgSignals.length * 10
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
if (results.length === 0) return null;
|
|
1001
|
+
results.sort((a, b) => b.score - a.score);
|
|
1002
|
+
const best = results[0];
|
|
1003
|
+
return { type: best.type, confidence: best.confidence, signals: best.signals };
|
|
1004
|
+
}
|
|
1005
|
+
function detectParams(source) {
|
|
1006
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1007
|
+
const regex = /(?:state\.PARAMS|PARAMS)\.(\w+)/g;
|
|
1008
|
+
let match;
|
|
1009
|
+
while ((match = regex.exec(source)) !== null) {
|
|
1010
|
+
keys.add(match[1]);
|
|
1011
|
+
}
|
|
1012
|
+
return [...keys];
|
|
1013
|
+
}
|
|
1014
|
+
function detectColorCount(source) {
|
|
1015
|
+
const indexRefs = /* @__PURE__ */ new Set();
|
|
1016
|
+
const keyRefs = /* @__PURE__ */ new Set();
|
|
1017
|
+
const indexRegex = /(?:state\.COLORS|COLORS)\[(\d+)\]/g;
|
|
1018
|
+
let match;
|
|
1019
|
+
while ((match = indexRegex.exec(source)) !== null) {
|
|
1020
|
+
indexRefs.add(Number(match[1]));
|
|
1021
|
+
}
|
|
1022
|
+
const keyRegex = /(?:state\.COLORS|COLORS)\.(\w+)/g;
|
|
1023
|
+
while ((match = keyRegex.exec(source)) !== null) {
|
|
1024
|
+
keyRefs.add(match[1]);
|
|
1025
|
+
}
|
|
1026
|
+
return Math.max(indexRefs.size, keyRefs.size);
|
|
1027
|
+
}
|
|
1028
|
+
function detectCanvasSize(source) {
|
|
1029
|
+
const match = source.match(/createCanvas\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)/);
|
|
1030
|
+
if (match) {
|
|
1031
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
1032
|
+
}
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// src/commands/import.ts
|
|
1037
|
+
var RENDERER_TYPES2 = ["p5", "canvas2d", "three", "glsl", "svg"];
|
|
1038
|
+
function slugify2(title) {
|
|
1039
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1040
|
+
}
|
|
1041
|
+
var importCommand = new Command8("import").description("Convert a source file (.js, .glsl) into a .genart sketch").argument("<files...>", "Source file(s) to import").option("--renderer <type>", "Force renderer type (skip auto-detection)").option("--preset <name>", "Canvas preset", "square-600").option("--title <string>", "Sketch title (skip prompt)").option("--seed <n>", "Initial seed", Number).option("-y, --non-interactive", "Accept all defaults, skip prompts").option("--batch", "Process multiple files non-interactively").option("--dry-run", "Show what would be generated without writing").option("-o, --output <path>", "Output path (single file only)").action(async (files, opts) => {
|
|
1042
|
+
const nonInteractive = !!(opts.nonInteractive || opts.batch);
|
|
1043
|
+
try {
|
|
1044
|
+
for (const file of files) {
|
|
1045
|
+
await importFile(file, opts, nonInteractive);
|
|
1046
|
+
}
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
console.error(chalk8.red(`Import failed: ${err.message}`));
|
|
1049
|
+
process.exitCode = 1;
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
async function importFile(file, opts, nonInteractive) {
|
|
1053
|
+
const spinner = ora6(`Importing ${basename5(file)}...`).start();
|
|
1054
|
+
const filePath = resolve8(file);
|
|
1055
|
+
const source = await readFile3(filePath, "utf-8");
|
|
1056
|
+
const ext = extname4(filePath).toLowerCase();
|
|
1057
|
+
let rendererType;
|
|
1058
|
+
if (opts.renderer) {
|
|
1059
|
+
rendererType = opts.renderer;
|
|
1060
|
+
if (!RENDERER_TYPES2.includes(rendererType)) {
|
|
1061
|
+
spinner.fail(chalk8.red(`Invalid renderer: ${rendererType}`));
|
|
1062
|
+
throw new Error(`Invalid renderer "${rendererType}". Choose from: ${RENDERER_TYPES2.join(", ")}`);
|
|
1063
|
+
}
|
|
1064
|
+
spinner.text = `Renderer: ${rendererType} (specified)`;
|
|
1065
|
+
} else {
|
|
1066
|
+
const detection = detectRenderer(source);
|
|
1067
|
+
if (detection) {
|
|
1068
|
+
spinner.text = `Detected renderer: ${detection.type} (${detection.confidence})`;
|
|
1069
|
+
if (detection.confidence === "low" && !nonInteractive) {
|
|
1070
|
+
spinner.stop();
|
|
1071
|
+
const { default: inquirer } = await import("inquirer");
|
|
1072
|
+
const answers = await inquirer.prompt([
|
|
1073
|
+
{
|
|
1074
|
+
type: "list",
|
|
1075
|
+
name: "renderer",
|
|
1076
|
+
message: `Low-confidence detection: ${detection.type}. Confirm or choose:`,
|
|
1077
|
+
choices: RENDERER_TYPES2.map((t) => ({ name: t, value: t })),
|
|
1078
|
+
default: detection.type
|
|
1079
|
+
}
|
|
1080
|
+
]);
|
|
1081
|
+
rendererType = answers.renderer;
|
|
1082
|
+
spinner.start();
|
|
1083
|
+
} else {
|
|
1084
|
+
rendererType = detection.type;
|
|
1085
|
+
}
|
|
1086
|
+
} else {
|
|
1087
|
+
if (ext === ".glsl" || ext === ".frag" || ext === ".vert") {
|
|
1088
|
+
rendererType = "glsl";
|
|
1089
|
+
} else if (nonInteractive) {
|
|
1090
|
+
rendererType = "p5";
|
|
1091
|
+
} else {
|
|
1092
|
+
spinner.stop();
|
|
1093
|
+
const { default: inquirer } = await import("inquirer");
|
|
1094
|
+
const answers = await inquirer.prompt([
|
|
1095
|
+
{
|
|
1096
|
+
type: "list",
|
|
1097
|
+
name: "renderer",
|
|
1098
|
+
message: "Could not detect renderer type. Please choose:",
|
|
1099
|
+
choices: RENDERER_TYPES2.map((t) => ({ name: t, value: t }))
|
|
1100
|
+
}
|
|
1101
|
+
]);
|
|
1102
|
+
rendererType = answers.renderer;
|
|
1103
|
+
spinner.start();
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
spinner.text = "Validating algorithm...";
|
|
1108
|
+
const registry = createDefaultRegistry6();
|
|
1109
|
+
const adapter = registry.resolve(rendererType);
|
|
1110
|
+
const validation = adapter.validate(source);
|
|
1111
|
+
if (!validation.valid) {
|
|
1112
|
+
spinner.warn(chalk8.yellow(`Validation warnings for ${rendererType}:`));
|
|
1113
|
+
for (const error of validation.errors) {
|
|
1114
|
+
console.error(chalk8.yellow(` - ${error}`));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const detectedParams = detectParams(source);
|
|
1118
|
+
const detectedColorCount = detectColorCount(source);
|
|
1119
|
+
const detectedSize = detectCanvasSize(source);
|
|
1120
|
+
let title;
|
|
1121
|
+
if (opts.title) {
|
|
1122
|
+
title = opts.title;
|
|
1123
|
+
} else if (nonInteractive) {
|
|
1124
|
+
title = basename5(filePath, extname4(filePath)).replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1125
|
+
} else {
|
|
1126
|
+
spinner.stop();
|
|
1127
|
+
const { default: inquirer } = await import("inquirer");
|
|
1128
|
+
const answers = await inquirer.prompt([
|
|
1129
|
+
{
|
|
1130
|
+
type: "input",
|
|
1131
|
+
name: "title",
|
|
1132
|
+
message: "Sketch title:",
|
|
1133
|
+
default: basename5(filePath, extname4(filePath)).replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
|
|
1134
|
+
}
|
|
1135
|
+
]);
|
|
1136
|
+
title = answers.title;
|
|
1137
|
+
spinner.start();
|
|
1138
|
+
}
|
|
1139
|
+
const preset = opts.preset;
|
|
1140
|
+
let canvasWidth;
|
|
1141
|
+
let canvasHeight;
|
|
1142
|
+
if (detectedSize && !opts.preset) {
|
|
1143
|
+
canvasWidth = detectedSize.width;
|
|
1144
|
+
canvasHeight = detectedSize.height;
|
|
1145
|
+
} else {
|
|
1146
|
+
const dims = resolvePreset3(preset);
|
|
1147
|
+
canvasWidth = dims.width;
|
|
1148
|
+
canvasHeight = dims.height;
|
|
1149
|
+
}
|
|
1150
|
+
const parameters = detectedParams.map((key) => ({
|
|
1151
|
+
key,
|
|
1152
|
+
label: key.charAt(0).toUpperCase() + key.slice(1),
|
|
1153
|
+
min: 0,
|
|
1154
|
+
max: 1,
|
|
1155
|
+
step: 0.01,
|
|
1156
|
+
default: 0.5
|
|
1157
|
+
}));
|
|
1158
|
+
const colors = [];
|
|
1159
|
+
const defaultColors = ["#FF6B35", "#004E89", "#F7C59F", "#1A936F", "#C6DABF"];
|
|
1160
|
+
for (let i = 0; i < detectedColorCount; i++) {
|
|
1161
|
+
colors.push({
|
|
1162
|
+
key: `color${i}`,
|
|
1163
|
+
label: `Color ${i + 1}`,
|
|
1164
|
+
default: defaultColors[i % defaultColors.length]
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
const seed = opts.seed ?? Math.floor(Math.random() * 1e4);
|
|
1168
|
+
const id = slugify2(title);
|
|
1169
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1170
|
+
const sketch = {
|
|
1171
|
+
genart: "1.0",
|
|
1172
|
+
id,
|
|
1173
|
+
title,
|
|
1174
|
+
created: now,
|
|
1175
|
+
modified: now,
|
|
1176
|
+
renderer: { type: rendererType },
|
|
1177
|
+
canvas: {
|
|
1178
|
+
preset: detectedSize ? void 0 : preset,
|
|
1179
|
+
width: canvasWidth,
|
|
1180
|
+
height: canvasHeight
|
|
1181
|
+
},
|
|
1182
|
+
parameters,
|
|
1183
|
+
colors,
|
|
1184
|
+
state: {
|
|
1185
|
+
seed,
|
|
1186
|
+
params: Object.fromEntries(parameters.map((p) => [p.key, p.default])),
|
|
1187
|
+
colorPalette: colors.map((c) => c.default)
|
|
1188
|
+
},
|
|
1189
|
+
algorithm: source
|
|
1190
|
+
};
|
|
1191
|
+
if (opts.dryRun) {
|
|
1192
|
+
spinner.stop();
|
|
1193
|
+
console.log(chalk8.dim("--- Dry run (would write): ---"));
|
|
1194
|
+
console.log(serializeGenart2(sketch));
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const outputPath = opts.output ? resolve8(opts.output) : resolve8(`${id}.genart`);
|
|
1198
|
+
const json = serializeGenart2(sketch);
|
|
1199
|
+
await writeFile6(outputPath, json, "utf-8");
|
|
1200
|
+
spinner.succeed(chalk8.green(`\u2713 Imported \u2192 ${outputPath}`));
|
|
1201
|
+
console.log(chalk8.dim(` Renderer: ${rendererType}`));
|
|
1202
|
+
console.log(chalk8.dim(` Canvas: ${canvasWidth}\xD7${canvasHeight}`));
|
|
1203
|
+
if (detectedParams.length > 0) {
|
|
1204
|
+
console.log(chalk8.dim(` Parameters: ${detectedParams.join(", ")}`));
|
|
1205
|
+
}
|
|
1206
|
+
if (detectedColorCount > 0) {
|
|
1207
|
+
console.log(chalk8.dim(` Colors: ${detectedColorCount} slot${detectedColorCount > 1 ? "s" : ""}`));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/commands/video.ts
|
|
1212
|
+
import { Command as Command9 } from "commander";
|
|
1213
|
+
import { resolve as resolve9, basename as basename6, extname as extname5 } from "path";
|
|
1214
|
+
import { stat as stat3 } from "fs/promises";
|
|
1215
|
+
import chalk9 from "chalk";
|
|
1216
|
+
import ora7 from "ora";
|
|
1217
|
+
import { createDefaultRegistry as createDefaultRegistry7 } from "@genart-dev/core";
|
|
1218
|
+
|
|
1219
|
+
// src/video/interpolate.ts
|
|
1220
|
+
var EASINGS = {
|
|
1221
|
+
linear: (t) => t,
|
|
1222
|
+
"ease-in": (t) => t * t,
|
|
1223
|
+
"ease-out": (t) => t * (2 - t),
|
|
1224
|
+
"ease-in-out": (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
|
|
1225
|
+
};
|
|
1226
|
+
function parseAnimate(value) {
|
|
1227
|
+
const eqIdx = value.indexOf("=");
|
|
1228
|
+
if (eqIdx < 1) {
|
|
1229
|
+
throw new Error(
|
|
1230
|
+
`Invalid --animate format: "${value}". Expected "param=start:end"`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
const key = value.slice(0, eqIdx);
|
|
1234
|
+
const range = value.slice(eqIdx + 1);
|
|
1235
|
+
const parts = range.split(":");
|
|
1236
|
+
if (parts.length !== 2) {
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
`Invalid --animate range: "${value}". Expected "param=start:end"`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
const start = Number(parts[0]);
|
|
1242
|
+
const end = Number(parts[1]);
|
|
1243
|
+
if (Number.isNaN(start) || Number.isNaN(end)) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
`Invalid --animate values: "${value}". Start and end must be numbers`
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
return { key, start, end };
|
|
1249
|
+
}
|
|
1250
|
+
function interpolateParams(specs, t, easing) {
|
|
1251
|
+
const easedT = easing(Math.max(0, Math.min(1, t)));
|
|
1252
|
+
const result = {};
|
|
1253
|
+
for (const spec of specs) {
|
|
1254
|
+
result[spec.key] = spec.start + (spec.end - spec.start) * easedT;
|
|
1255
|
+
}
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
function collectAnimates(value, prev) {
|
|
1259
|
+
return [...prev, value];
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// src/video/ffmpeg.ts
|
|
1263
|
+
import { execSync, spawn } from "child_process";
|
|
1264
|
+
function findFfmpeg() {
|
|
1265
|
+
const envPath = process.env["GENART_FFMPEG_PATH"];
|
|
1266
|
+
if (envPath) return envPath;
|
|
1267
|
+
try {
|
|
1268
|
+
return execSync("which ffmpeg", { encoding: "utf-8" }).trim();
|
|
1269
|
+
} catch {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
"ffmpeg not found. Install ffmpeg or set GENART_FFMPEG_PATH.\n macOS: brew install ffmpeg\n Linux: sudo apt install ffmpeg\n Or: GENART_FFMPEG_PATH=/path/to/ffmpeg genart video ..."
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function qualityToCrf(quality, codec) {
|
|
1276
|
+
const maxCrf = codec === "libvpx-vp9" ? 63 : 51;
|
|
1277
|
+
return Math.round((100 - quality) * maxCrf / 100);
|
|
1278
|
+
}
|
|
1279
|
+
function resolveCodec(codec) {
|
|
1280
|
+
const map = {
|
|
1281
|
+
h264: "libx264",
|
|
1282
|
+
h265: "libx265",
|
|
1283
|
+
vp9: "libvpx-vp9"
|
|
1284
|
+
};
|
|
1285
|
+
return map[codec] ?? codec;
|
|
1286
|
+
}
|
|
1287
|
+
function buildFfmpegArgs(opts) {
|
|
1288
|
+
const args = [
|
|
1289
|
+
// Input: piped PNG frames
|
|
1290
|
+
"-f",
|
|
1291
|
+
"image2pipe",
|
|
1292
|
+
"-framerate",
|
|
1293
|
+
String(opts.fps),
|
|
1294
|
+
"-i",
|
|
1295
|
+
"pipe:0",
|
|
1296
|
+
// Overwrite output
|
|
1297
|
+
"-y"
|
|
1298
|
+
];
|
|
1299
|
+
if (opts.format === "gif") {
|
|
1300
|
+
args.push("-vf", `fps=${opts.fps}`);
|
|
1301
|
+
if (opts.loop !== void 0) {
|
|
1302
|
+
args.push("-loop", String(opts.loop));
|
|
1303
|
+
}
|
|
1304
|
+
} else {
|
|
1305
|
+
const ffCodec = resolveCodec(opts.codec);
|
|
1306
|
+
const crf = qualityToCrf(opts.quality, ffCodec);
|
|
1307
|
+
args.push("-c:v", ffCodec, "-crf", String(crf));
|
|
1308
|
+
if (ffCodec === "libx264" || ffCodec === "libx265") {
|
|
1309
|
+
args.push("-pix_fmt", "yuv420p");
|
|
1310
|
+
}
|
|
1311
|
+
if (ffCodec === "libx264") {
|
|
1312
|
+
args.push("-movflags", "+faststart");
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
args.push(opts.output);
|
|
1316
|
+
return args;
|
|
1317
|
+
}
|
|
1318
|
+
function spawnFfmpeg(ffmpegPath, args) {
|
|
1319
|
+
return spawn(ffmpegPath, args, {
|
|
1320
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/video/time-inject.ts
|
|
1325
|
+
function buildTimeOffsetScript(offsetMs) {
|
|
1326
|
+
return `
|
|
1327
|
+
(() => {
|
|
1328
|
+
const __OFFSET = ${offsetMs};
|
|
1329
|
+
const __origPerfNow = performance.now.bind(performance);
|
|
1330
|
+
const __origDateNow = Date.now.bind(Date);
|
|
1331
|
+
const __origRAF = window.requestAnimationFrame.bind(window);
|
|
1332
|
+
performance.now = () => __origPerfNow() + __OFFSET;
|
|
1333
|
+
Date.now = () => __origDateNow() + __OFFSET;
|
|
1334
|
+
window.requestAnimationFrame = (cb) => __origRAF((ts) => cb(ts + __OFFSET));
|
|
1335
|
+
})();
|
|
1336
|
+
`;
|
|
1337
|
+
}
|
|
1338
|
+
async function injectTimeOffset(page, offsetMs) {
|
|
1339
|
+
await page.evaluate(buildTimeOffsetScript(offsetMs));
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// src/commands/video.ts
|
|
1343
|
+
var videoCommand = new Command9("video").description("Render a video from an animated sketch").argument("<file>", "Path to .genart file").requiredOption("--duration <seconds>", "Video duration in seconds", Number).option("--fps <n>", "Frames per second", Number, 30).option("--format <fmt>", "Output format: mp4, webm, gif", "mp4").option("--codec <name>", "Video codec: h264, h265, vp9", "h264").option("--quality <n>", "Encoding quality (0-100)", Number, 75).option(
|
|
1344
|
+
"--animate <spec>",
|
|
1345
|
+
"Interpolate parameter: param=start:end (repeatable)",
|
|
1346
|
+
collectAnimates,
|
|
1347
|
+
[]
|
|
1348
|
+
).option("--easing <fn>", "Easing function: linear, ease-in, ease-out, ease-in-out", "linear").option("--loop <n>", "GIF loop count (0=infinite)", Number, 0).option("--concurrency <n>", "Parallel frame captures", Number, 4).option("--wait <duration>", "Init wait before time injection", "200ms").option("--seed <n>", "Override seed", Number).option("--params <json>", "Override parameters (JSON object)").option("--colors <json>", "Override color palette (JSON array)").option("--width <n>", "Override canvas width", Number).option("--height <n>", "Override canvas height", Number).option("--preset <name>", "Use a canvas preset").option("-o, --output <path>", "Output file path").action(async (file, opts) => {
|
|
1349
|
+
const spinner = ora7("Loading sketch...").start();
|
|
1350
|
+
try {
|
|
1351
|
+
const filePath = resolve9(file);
|
|
1352
|
+
const sketch = await loadSketch(filePath);
|
|
1353
|
+
if (sketch.renderer.type === "svg") {
|
|
1354
|
+
throw new Error(
|
|
1355
|
+
"SVG sketches are static and cannot be animated. The video command requires an animated renderer (p5, canvas2d, three, glsl)."
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
const overrides = {};
|
|
1359
|
+
if (opts.seed !== void 0) overrides.seed = opts.seed;
|
|
1360
|
+
if (opts.width !== void 0) overrides.width = opts.width;
|
|
1361
|
+
if (opts.height !== void 0) overrides.height = opts.height;
|
|
1362
|
+
if (opts.preset) overrides.preset = opts.preset;
|
|
1363
|
+
if (opts.params) {
|
|
1364
|
+
overrides.params = JSON.parse(opts.params);
|
|
1365
|
+
}
|
|
1366
|
+
if (opts.colors) {
|
|
1367
|
+
overrides.colors = JSON.parse(opts.colors);
|
|
1368
|
+
}
|
|
1369
|
+
const modified = applyOverrides(sketch, overrides);
|
|
1370
|
+
const registry = createDefaultRegistry7();
|
|
1371
|
+
const adapter = registry.resolve(modified.renderer.type);
|
|
1372
|
+
spinner.text = "Detecting ffmpeg...";
|
|
1373
|
+
const ffmpegPath = findFfmpeg();
|
|
1374
|
+
const animateSpecs = opts.animate.map(parseAnimate);
|
|
1375
|
+
const easingName = opts.easing;
|
|
1376
|
+
const easing = EASINGS[easingName];
|
|
1377
|
+
if (!easing) {
|
|
1378
|
+
throw new Error(
|
|
1379
|
+
`Unknown easing function: "${easingName}". Available: ${Object.keys(EASINGS).join(", ")}`
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
const duration = opts.duration;
|
|
1383
|
+
const fps = opts.fps;
|
|
1384
|
+
const totalFrames = Math.ceil(duration * fps);
|
|
1385
|
+
const concurrency = opts.concurrency;
|
|
1386
|
+
const initWaitMs = parseWait(opts.wait);
|
|
1387
|
+
const format = opts.format;
|
|
1388
|
+
const outputPath = resolve9(
|
|
1389
|
+
opts.output ?? `${basename6(filePath, extname5(filePath))}.${format}`
|
|
1390
|
+
);
|
|
1391
|
+
spinner.text = `Video: starting ffmpeg (${totalFrames} frames, ${fps}fps)...`;
|
|
1392
|
+
const ffmpegArgs = buildFfmpegArgs({
|
|
1393
|
+
output: outputPath,
|
|
1394
|
+
width: modified.canvas.width,
|
|
1395
|
+
height: modified.canvas.height,
|
|
1396
|
+
fps,
|
|
1397
|
+
format,
|
|
1398
|
+
codec: opts.codec,
|
|
1399
|
+
quality: opts.quality,
|
|
1400
|
+
loop: opts.loop
|
|
1401
|
+
});
|
|
1402
|
+
const ffmpegProc = spawnFfmpeg(ffmpegPath, ffmpegArgs);
|
|
1403
|
+
let ffmpegStderr = "";
|
|
1404
|
+
ffmpegProc.stderr?.on("data", (chunk) => {
|
|
1405
|
+
ffmpegStderr += chunk.toString();
|
|
1406
|
+
});
|
|
1407
|
+
const hasAnimatedParams = animateSpecs.length > 0;
|
|
1408
|
+
let framesRendered = 0;
|
|
1409
|
+
for (let i = 0; i < totalFrames; i += concurrency) {
|
|
1410
|
+
const chunkSize = Math.min(concurrency, totalFrames - i);
|
|
1411
|
+
const frameIndices = Array.from({ length: chunkSize }, (_, j) => i + j);
|
|
1412
|
+
const frames = await Promise.all(
|
|
1413
|
+
frameIndices.map(async (frameIdx) => {
|
|
1414
|
+
const t = totalFrames <= 1 ? 0 : frameIdx / (totalFrames - 1);
|
|
1415
|
+
const timeOffsetMs = frameIdx / fps * 1e3;
|
|
1416
|
+
let frameSketch = modified;
|
|
1417
|
+
if (hasAnimatedParams) {
|
|
1418
|
+
const animatedParams = interpolateParams(animateSpecs, t, easing);
|
|
1419
|
+
frameSketch = applyOverrides(modified, { params: animatedParams });
|
|
1420
|
+
}
|
|
1421
|
+
const html = adapter.generateStandaloneHTML(frameSketch);
|
|
1422
|
+
const page = await getPage(
|
|
1423
|
+
frameSketch.canvas.width,
|
|
1424
|
+
frameSketch.canvas.height
|
|
1425
|
+
);
|
|
1426
|
+
try {
|
|
1427
|
+
await page.setContent(html, {
|
|
1428
|
+
waitUntil: "domcontentloaded",
|
|
1429
|
+
timeout: 3e4
|
|
1430
|
+
});
|
|
1431
|
+
if (initWaitMs > 0) {
|
|
1432
|
+
await new Promise((resolve10) => setTimeout(resolve10, initWaitMs));
|
|
1433
|
+
}
|
|
1434
|
+
if (timeOffsetMs > 0) {
|
|
1435
|
+
await injectTimeOffset(page, timeOffsetMs);
|
|
1436
|
+
await page.evaluate(
|
|
1437
|
+
() => new Promise(
|
|
1438
|
+
(resolve10) => requestAnimationFrame(() => resolve10())
|
|
1439
|
+
)
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
const buffer = await page.screenshot({
|
|
1443
|
+
type: "png",
|
|
1444
|
+
clip: {
|
|
1445
|
+
x: 0,
|
|
1446
|
+
y: 0,
|
|
1447
|
+
width: frameSketch.canvas.width,
|
|
1448
|
+
height: frameSketch.canvas.height
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
return new Uint8Array(buffer);
|
|
1452
|
+
} finally {
|
|
1453
|
+
await page.close();
|
|
1454
|
+
}
|
|
1455
|
+
})
|
|
1456
|
+
);
|
|
1457
|
+
for (const frame of frames) {
|
|
1458
|
+
const canWrite = ffmpegProc.stdin.write(frame);
|
|
1459
|
+
if (!canWrite) {
|
|
1460
|
+
await new Promise(
|
|
1461
|
+
(resolve10) => ffmpegProc.stdin.once("drain", resolve10)
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
framesRendered += chunkSize;
|
|
1466
|
+
const pct = Math.round(framesRendered / totalFrames * 100);
|
|
1467
|
+
spinner.text = `Video: ${framesRendered}/${totalFrames} frames (${pct}%)`;
|
|
1468
|
+
}
|
|
1469
|
+
ffmpegProc.stdin.end();
|
|
1470
|
+
const exitCode = await new Promise((resolve10, reject) => {
|
|
1471
|
+
ffmpegProc.on("close", (code) => resolve10(code ?? 0));
|
|
1472
|
+
ffmpegProc.on("error", reject);
|
|
1473
|
+
});
|
|
1474
|
+
if (exitCode !== 0) {
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
`ffmpeg exited with code ${exitCode}:
|
|
1477
|
+
${ffmpegStderr.slice(-500)}`
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
const fileSize = (await stat3(outputPath)).size;
|
|
1481
|
+
const sizeMB = (fileSize / (1024 * 1024)).toFixed(1);
|
|
1482
|
+
spinner.succeed(
|
|
1483
|
+
chalk9.green(
|
|
1484
|
+
`Video: ${modified.canvas.width}\xD7${modified.canvas.height}, ${totalFrames} frames, ${duration}s \u2192 ${outputPath} (${sizeMB} MB)`
|
|
1485
|
+
)
|
|
1486
|
+
);
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
spinner.fail(chalk9.red(`Video failed: ${err.message}`));
|
|
1489
|
+
process.exitCode = 1;
|
|
1490
|
+
} finally {
|
|
1491
|
+
await closeBrowser();
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
// src/commands/agent/index.ts
|
|
1496
|
+
import { Command as Command15 } from "commander";
|
|
1497
|
+
|
|
1498
|
+
// src/commands/agent/stdio.ts
|
|
1499
|
+
import { Command as Command10 } from "commander";
|
|
1500
|
+
var stdioCommand = new Command10("stdio").description("Start MCP server over stdio transport").option("--base-path <dir>", "Base directory for file operations", process.cwd()).action(async (opts) => {
|
|
1501
|
+
const { EditorState, createServer } = await import("@genart-dev/mcp-server/lib");
|
|
1502
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1503
|
+
const state = new EditorState();
|
|
1504
|
+
if (opts.basePath) {
|
|
1505
|
+
state.basePath = opts.basePath;
|
|
1506
|
+
}
|
|
1507
|
+
const server = createServer(state);
|
|
1508
|
+
const transport = new StdioServerTransport();
|
|
1509
|
+
await server.connect(transport);
|
|
1510
|
+
console.error("[genart] MCP server connected (stdio)");
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
// src/commands/agent/http.ts
|
|
1514
|
+
import { Command as Command11 } from "commander";
|
|
1515
|
+
import {
|
|
1516
|
+
createServer as createHttpServer
|
|
1517
|
+
} from "http";
|
|
1518
|
+
import chalk10 from "chalk";
|
|
1519
|
+
var httpCommand = new Command11("http").description("Start MCP server over HTTP (StreamableHTTP transport)").option("--port <n>", "Port to listen on", Number, 3333).option("--host <addr>", "Host to bind to", "127.0.0.1").option("--base-path <dir>", "Base directory for file operations", process.cwd()).option("--cors", "Enable CORS headers for browser access").action(async (opts) => {
|
|
1520
|
+
const { EditorState, createServer } = await import("@genart-dev/mcp-server/lib");
|
|
1521
|
+
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
1522
|
+
const port = opts.port;
|
|
1523
|
+
const host = opts.host;
|
|
1524
|
+
const enableCors = opts.cors;
|
|
1525
|
+
const state = new EditorState();
|
|
1526
|
+
if (opts.basePath) {
|
|
1527
|
+
state.basePath = opts.basePath;
|
|
1528
|
+
}
|
|
1529
|
+
const mcpServer = createServer(state);
|
|
1530
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1531
|
+
sessionIdGenerator: () => "local"
|
|
1532
|
+
});
|
|
1533
|
+
await mcpServer.connect(transport);
|
|
1534
|
+
const httpServer = createHttpServer(
|
|
1535
|
+
async (req, res) => {
|
|
1536
|
+
if (enableCors) {
|
|
1537
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1538
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
1539
|
+
res.setHeader(
|
|
1540
|
+
"Access-Control-Allow-Headers",
|
|
1541
|
+
"Content-Type, Mcp-Session-Id"
|
|
1542
|
+
);
|
|
1543
|
+
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
|
|
1544
|
+
}
|
|
1545
|
+
if (req.method === "OPTIONS") {
|
|
1546
|
+
res.writeHead(enableCors ? 204 : 405);
|
|
1547
|
+
res.end();
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (req.url === "/health") {
|
|
1551
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1552
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
if (req.url === "/mcp") {
|
|
1556
|
+
const chunks = [];
|
|
1557
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1558
|
+
req.on("end", async () => {
|
|
1559
|
+
const raw = Buffer.concat(chunks).toString();
|
|
1560
|
+
const body = raw ? JSON.parse(raw) : void 0;
|
|
1561
|
+
try {
|
|
1562
|
+
await transport.handleRequest(req, res, body);
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
console.error("[genart] handleRequest error:", err);
|
|
1565
|
+
if (!res.writableEnded) {
|
|
1566
|
+
res.writeHead(500);
|
|
1567
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
res.writeHead(404);
|
|
1574
|
+
res.end();
|
|
1575
|
+
}
|
|
1576
|
+
);
|
|
1577
|
+
httpServer.on("error", (err) => {
|
|
1578
|
+
if (err.code === "EADDRINUSE") {
|
|
1579
|
+
console.error(
|
|
1580
|
+
chalk10.red(`Port ${port} is already in use.`) + "\n" + chalk10.dim(`Try: genart agent http --port ${port + 1}`)
|
|
1581
|
+
);
|
|
1582
|
+
process.exitCode = 1;
|
|
1583
|
+
} else {
|
|
1584
|
+
console.error(chalk10.red(`Server error: ${err.message}`));
|
|
1585
|
+
process.exitCode = 1;
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
httpServer.listen(port, host, () => {
|
|
1589
|
+
console.log(
|
|
1590
|
+
chalk10.green(`MCP server listening on http://${host}:${port}/mcp`)
|
|
1591
|
+
);
|
|
1592
|
+
if (enableCors) {
|
|
1593
|
+
console.log(chalk10.dim("CORS enabled"));
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
const shutdown = async () => {
|
|
1597
|
+
console.log(chalk10.dim("\nShutting down..."));
|
|
1598
|
+
httpServer.close();
|
|
1599
|
+
try {
|
|
1600
|
+
await mcpServer.close();
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
process.exit(0);
|
|
1604
|
+
};
|
|
1605
|
+
process.on("SIGINT", shutdown);
|
|
1606
|
+
process.on("SIGTERM", shutdown);
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// src/commands/agent/sidecar.ts
|
|
1610
|
+
import { Command as Command12 } from "commander";
|
|
1611
|
+
var sidecarCommand = new Command12("sidecar").description("Start MCP server in sidecar mode (stdio + IPC mutations)").option("--base-path <dir>", "Base directory for file operations", process.cwd()).action(async (opts) => {
|
|
1612
|
+
process.env["GENART_SIDECAR"] = "1";
|
|
1613
|
+
const { EditorState, createServer } = await import("@genart-dev/mcp-server/lib");
|
|
1614
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1615
|
+
const state = new EditorState();
|
|
1616
|
+
if (opts.basePath) {
|
|
1617
|
+
state.basePath = opts.basePath;
|
|
1618
|
+
}
|
|
1619
|
+
const server = createServer(state);
|
|
1620
|
+
const transport = new StdioServerTransport();
|
|
1621
|
+
await server.connect(transport);
|
|
1622
|
+
console.error("[genart] MCP server connected (sidecar)");
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
// src/commands/agent/install.ts
|
|
1626
|
+
import { Command as Command13 } from "commander";
|
|
1627
|
+
import chalk11 from "chalk";
|
|
1628
|
+
|
|
1629
|
+
// src/commands/agent/clients.ts
|
|
1630
|
+
import { readFile as readFile4, writeFile as writeFile7, mkdir as mkdir2 } from "fs/promises";
|
|
1631
|
+
import { execSync as execSync2 } from "child_process";
|
|
1632
|
+
import { homedir } from "os";
|
|
1633
|
+
import { dirname, join } from "path";
|
|
1634
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
1635
|
+
var CLIENTS = [
|
|
1636
|
+
{
|
|
1637
|
+
id: "claude",
|
|
1638
|
+
name: "Claude Code",
|
|
1639
|
+
configRelPath: ".claude.json",
|
|
1640
|
+
format: "json",
|
|
1641
|
+
serversKey: "mcpServers",
|
|
1642
|
+
binaryName: "claude"
|
|
1643
|
+
},
|
|
1644
|
+
{
|
|
1645
|
+
id: "codex",
|
|
1646
|
+
name: "Codex CLI",
|
|
1647
|
+
configRelPath: ".codex/config.toml",
|
|
1648
|
+
format: "toml",
|
|
1649
|
+
serversKey: "mcp_servers",
|
|
1650
|
+
binaryName: "codex"
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
id: "cursor",
|
|
1654
|
+
name: "Cursor",
|
|
1655
|
+
configRelPath: ".cursor/mcp.json",
|
|
1656
|
+
format: "json",
|
|
1657
|
+
serversKey: "mcpServers",
|
|
1658
|
+
binaryName: "cursor"
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
id: "vscode",
|
|
1662
|
+
name: "VS Code",
|
|
1663
|
+
configRelPath: process.platform === "darwin" ? "Library/Application Support/Code/User/settings.json" : process.platform === "win32" ? "AppData/Roaming/Code/User/settings.json" : ".config/Code/User/settings.json",
|
|
1664
|
+
format: "json",
|
|
1665
|
+
serversKey: "mcp.servers",
|
|
1666
|
+
binaryName: "code"
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
id: "gemini",
|
|
1670
|
+
name: "Gemini CLI",
|
|
1671
|
+
configRelPath: ".gemini/settings.json",
|
|
1672
|
+
format: "json",
|
|
1673
|
+
serversKey: "mcpServers",
|
|
1674
|
+
binaryName: "gemini"
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
id: "opencode",
|
|
1678
|
+
name: "OpenCode",
|
|
1679
|
+
configRelPath: ".config/opencode/opencode.json",
|
|
1680
|
+
format: "json",
|
|
1681
|
+
serversKey: "mcp",
|
|
1682
|
+
binaryName: "opencode"
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
id: "kiro",
|
|
1686
|
+
name: "Kiro",
|
|
1687
|
+
configRelPath: ".kiro/settings/mcp.json",
|
|
1688
|
+
format: "json",
|
|
1689
|
+
serversKey: "mcpServers",
|
|
1690
|
+
binaryName: "kiro"
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
id: "windsurf",
|
|
1694
|
+
name: "Windsurf",
|
|
1695
|
+
configRelPath: ".codeium/windsurf/mcp_config.json",
|
|
1696
|
+
format: "json",
|
|
1697
|
+
serversKey: "mcpServers",
|
|
1698
|
+
binaryName: "windsurf"
|
|
1699
|
+
}
|
|
1700
|
+
];
|
|
1701
|
+
function findClient(id) {
|
|
1702
|
+
return CLIENTS.find((c) => c.id === id);
|
|
1703
|
+
}
|
|
1704
|
+
function getConfigPath(client) {
|
|
1705
|
+
return join(homedir(), client.configRelPath);
|
|
1706
|
+
}
|
|
1707
|
+
function isInPath(binary) {
|
|
1708
|
+
try {
|
|
1709
|
+
const cmd = process.platform === "win32" ? `where.exe ${binary}` : `which ${binary}`;
|
|
1710
|
+
execSync2(cmd, { stdio: "pipe" });
|
|
1711
|
+
return true;
|
|
1712
|
+
} catch {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
function detectGenartBin() {
|
|
1717
|
+
if (isInPath("genart")) {
|
|
1718
|
+
return { command: "genart", args: ["agent", "stdio"] };
|
|
1719
|
+
}
|
|
1720
|
+
return { command: "npx", args: ["-y", "@genart-dev/cli", "agent", "stdio"] };
|
|
1721
|
+
}
|
|
1722
|
+
function getNestedValue(obj, dotPath) {
|
|
1723
|
+
const parts = dotPath.split(".");
|
|
1724
|
+
let current = obj;
|
|
1725
|
+
for (const part of parts) {
|
|
1726
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
1727
|
+
return void 0;
|
|
1728
|
+
}
|
|
1729
|
+
current = current[part];
|
|
1730
|
+
}
|
|
1731
|
+
return current;
|
|
1732
|
+
}
|
|
1733
|
+
function setNestedValue(obj, dotPath, value) {
|
|
1734
|
+
const parts = dotPath.split(".");
|
|
1735
|
+
let current = obj;
|
|
1736
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1737
|
+
const part = parts[i];
|
|
1738
|
+
if (!current[part] || typeof current[part] !== "object") {
|
|
1739
|
+
current[part] = {};
|
|
1740
|
+
}
|
|
1741
|
+
current = current[part];
|
|
1742
|
+
}
|
|
1743
|
+
current[parts[parts.length - 1]] = value;
|
|
1744
|
+
}
|
|
1745
|
+
function deleteNestedValue(obj, dotPath, childKey) {
|
|
1746
|
+
const servers = getNestedValue(obj, dotPath);
|
|
1747
|
+
if (!servers || typeof servers !== "object") return;
|
|
1748
|
+
delete servers[childKey];
|
|
1749
|
+
if (Object.keys(servers).length === 0) {
|
|
1750
|
+
const parts = dotPath.split(".");
|
|
1751
|
+
if (parts.length === 1) {
|
|
1752
|
+
delete obj[parts[0]];
|
|
1753
|
+
} else {
|
|
1754
|
+
const parentPath = parts.slice(0, -1).join(".");
|
|
1755
|
+
const lastKey = parts[parts.length - 1];
|
|
1756
|
+
const parent = getNestedValue(obj, parentPath);
|
|
1757
|
+
if (parent) {
|
|
1758
|
+
delete parent[lastKey];
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
async function readJsonConfig(path) {
|
|
1764
|
+
try {
|
|
1765
|
+
const raw = await readFile4(path, "utf-8");
|
|
1766
|
+
return JSON.parse(raw);
|
|
1767
|
+
} catch (err) {
|
|
1768
|
+
if (err.code === "ENOENT") return {};
|
|
1769
|
+
throw err;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
async function writeJsonConfig(path, data) {
|
|
1773
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
1774
|
+
await writeFile7(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1775
|
+
}
|
|
1776
|
+
async function readTomlConfig(path) {
|
|
1777
|
+
try {
|
|
1778
|
+
const raw = await readFile4(path, "utf-8");
|
|
1779
|
+
return parseToml(raw);
|
|
1780
|
+
} catch (err) {
|
|
1781
|
+
if (err.code === "ENOENT") return {};
|
|
1782
|
+
throw err;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
async function writeTomlConfig(path, data) {
|
|
1786
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
1787
|
+
await writeFile7(path, stringifyToml(data) + "\n", "utf-8");
|
|
1788
|
+
}
|
|
1789
|
+
async function readClientConfig(client) {
|
|
1790
|
+
const path = getConfigPath(client);
|
|
1791
|
+
return client.format === "toml" ? readTomlConfig(path) : readJsonConfig(path);
|
|
1792
|
+
}
|
|
1793
|
+
async function writeClientConfig(client, data) {
|
|
1794
|
+
const path = getConfigPath(client);
|
|
1795
|
+
return client.format === "toml" ? writeTomlConfig(path, data) : writeJsonConfig(path, data);
|
|
1796
|
+
}
|
|
1797
|
+
async function isConfigured(client) {
|
|
1798
|
+
const config = await readClientConfig(client);
|
|
1799
|
+
const servers = getNestedValue(config, client.serversKey);
|
|
1800
|
+
if (!servers || typeof servers !== "object") return false;
|
|
1801
|
+
return "genart" in servers;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// src/commands/agent/install.ts
|
|
1805
|
+
var installCommand = new Command13("install").description("Configure MCP for an AI client").argument("[client]", "Client to configure (claude, codex, cursor, vscode, gemini, opencode, kiro, windsurf)").option("--all", "Configure all detected clients").option("--remove", "Remove genart configuration").option("--dry-run", "Preview changes without writing").option("--npx", "Force npx invocation instead of global binary").action(async (clientArg, opts) => {
|
|
1806
|
+
const isRemove = opts.remove;
|
|
1807
|
+
const isDryRun = opts.dryRun;
|
|
1808
|
+
const forceNpx = opts.npx;
|
|
1809
|
+
const installAll = opts.all;
|
|
1810
|
+
let targets;
|
|
1811
|
+
if (installAll) {
|
|
1812
|
+
targets = CLIENTS.filter((c) => isInPath(c.binaryName));
|
|
1813
|
+
if (targets.length === 0) {
|
|
1814
|
+
console.log(chalk11.yellow("No supported AI clients detected in PATH."));
|
|
1815
|
+
console.log(
|
|
1816
|
+
chalk11.dim(
|
|
1817
|
+
"Supported: " + CLIENTS.map((c) => c.id).join(", ")
|
|
1818
|
+
)
|
|
1819
|
+
);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
} else if (clientArg) {
|
|
1823
|
+
const client = findClient(clientArg);
|
|
1824
|
+
if (!client) {
|
|
1825
|
+
console.error(
|
|
1826
|
+
chalk11.red(`Unknown client: ${clientArg}`) + "\n" + chalk11.dim(
|
|
1827
|
+
"Available: " + CLIENTS.map((c) => c.id).join(", ")
|
|
1828
|
+
)
|
|
1829
|
+
);
|
|
1830
|
+
process.exitCode = 1;
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
targets = [client];
|
|
1834
|
+
} else {
|
|
1835
|
+
console.error(
|
|
1836
|
+
chalk11.red("Specify a client or use --all") + "\n" + chalk11.dim("Usage: genart agent install <client>") + "\n" + chalk11.dim(" genart agent install --all") + "\n\n" + chalk11.dim("Available: " + CLIENTS.map((c) => c.id).join(", "))
|
|
1837
|
+
);
|
|
1838
|
+
process.exitCode = 1;
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const bin = forceNpx ? { command: "npx", args: ["-y", "@genart-dev/cli", "agent", "stdio"] } : detectGenartBin();
|
|
1842
|
+
const entry = { command: bin.command, args: bin.args };
|
|
1843
|
+
for (const client of targets) {
|
|
1844
|
+
const configPath = getConfigPath(client);
|
|
1845
|
+
try {
|
|
1846
|
+
if (isRemove) {
|
|
1847
|
+
await removeClient(client, configPath, isDryRun);
|
|
1848
|
+
} else {
|
|
1849
|
+
await installClient(client, configPath, entry, isDryRun);
|
|
1850
|
+
}
|
|
1851
|
+
} catch (err) {
|
|
1852
|
+
const code = err.code;
|
|
1853
|
+
if (code === "EACCES") {
|
|
1854
|
+
console.error(
|
|
1855
|
+
chalk11.red(`Permission denied: ${configPath}`) + "\n" + chalk11.dim("Check file permissions and try again.")
|
|
1856
|
+
);
|
|
1857
|
+
} else {
|
|
1858
|
+
console.error(
|
|
1859
|
+
chalk11.red(`Failed to configure ${client.name}: ${err.message}`)
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
process.exitCode = 1;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
async function installClient(client, configPath, entry, isDryRun) {
|
|
1867
|
+
const config = await readClientConfig(client);
|
|
1868
|
+
let servers = getNestedValue(config, client.serversKey);
|
|
1869
|
+
if (!servers || typeof servers !== "object") {
|
|
1870
|
+
servers = {};
|
|
1871
|
+
setNestedValue(config, client.serversKey, servers);
|
|
1872
|
+
}
|
|
1873
|
+
servers["genart"] = entry;
|
|
1874
|
+
if (isDryRun) {
|
|
1875
|
+
console.log(chalk11.dim(`[dry-run] ${client.name} (${configPath}):`));
|
|
1876
|
+
if (client.format === "toml") {
|
|
1877
|
+
const { stringify } = await import("smol-toml");
|
|
1878
|
+
console.log(chalk11.dim(stringify(config)));
|
|
1879
|
+
} else {
|
|
1880
|
+
console.log(chalk11.dim(JSON.stringify(config, null, 2)));
|
|
1881
|
+
}
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
await writeClientConfig(client, config);
|
|
1885
|
+
console.log(
|
|
1886
|
+
chalk11.green(`Configured ${client.name}`) + chalk11.dim(` (${configPath})`)
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
async function removeClient(client, configPath, isDryRun) {
|
|
1890
|
+
const config = await readClientConfig(client);
|
|
1891
|
+
const servers = getNestedValue(config, client.serversKey);
|
|
1892
|
+
if (!servers || !("genart" in servers)) {
|
|
1893
|
+
console.log(chalk11.dim(`${client.name}: not configured, nothing to remove`));
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
deleteNestedValue(config, client.serversKey, "genart");
|
|
1897
|
+
if (isDryRun) {
|
|
1898
|
+
console.log(chalk11.dim(`[dry-run] Would remove genart from ${client.name} (${configPath})`));
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
await writeClientConfig(client, config);
|
|
1902
|
+
console.log(
|
|
1903
|
+
chalk11.green(`Removed genart from ${client.name}`) + chalk11.dim(` (${configPath})`)
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// src/commands/agent/doctor.ts
|
|
1908
|
+
import { Command as Command14 } from "commander";
|
|
1909
|
+
import { readFileSync } from "fs";
|
|
1910
|
+
import { join as join2 } from "path";
|
|
1911
|
+
import chalk12 from "chalk";
|
|
1912
|
+
var doctorCommand = new Command14("doctor").description("Diagnose genart MCP setup").action(async () => {
|
|
1913
|
+
console.log("\n Checking genart MCP setup...\n");
|
|
1914
|
+
let warnings = 0;
|
|
1915
|
+
const pkgPath = join2(import.meta.dirname, "../../../package.json");
|
|
1916
|
+
let version = "unknown";
|
|
1917
|
+
try {
|
|
1918
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1919
|
+
version = pkg.version;
|
|
1920
|
+
} catch {
|
|
1921
|
+
version = "0.1.0";
|
|
1922
|
+
}
|
|
1923
|
+
printCheck(true, `@genart-dev/cli installed (v${version})`);
|
|
1924
|
+
let mcpVersion = "";
|
|
1925
|
+
try {
|
|
1926
|
+
const mcpPkgPath = __require.resolve("@genart-dev/mcp-server/package.json", {
|
|
1927
|
+
paths: [process.cwd(), import.meta.dirname]
|
|
1928
|
+
});
|
|
1929
|
+
const mcpPkg = JSON.parse(readFileSync(mcpPkgPath, "utf-8"));
|
|
1930
|
+
mcpVersion = mcpPkg.version;
|
|
1931
|
+
printCheck(true, `@genart-dev/mcp-server resolved (v${mcpVersion})`);
|
|
1932
|
+
} catch {
|
|
1933
|
+
try {
|
|
1934
|
+
await import("@genart-dev/mcp-server/lib");
|
|
1935
|
+
printCheck(true, "@genart-dev/mcp-server resolved");
|
|
1936
|
+
} catch {
|
|
1937
|
+
printCheck(false, "@genart-dev/mcp-server not found");
|
|
1938
|
+
warnings++;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
const chromePath = findChromePath();
|
|
1942
|
+
if (chromePath) {
|
|
1943
|
+
printCheck(true, `Chrome available (${chromePath})`);
|
|
1944
|
+
} else {
|
|
1945
|
+
printWarning("Chrome not found (render/capture commands unavailable)");
|
|
1946
|
+
warnings++;
|
|
1947
|
+
}
|
|
1948
|
+
if (isInPath("ffmpeg")) {
|
|
1949
|
+
printCheck(true, "ffmpeg available");
|
|
1950
|
+
} else {
|
|
1951
|
+
printWarning("ffmpeg not installed (video command unavailable)");
|
|
1952
|
+
warnings++;
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
await import("sharp");
|
|
1956
|
+
printCheck(true, "sharp available");
|
|
1957
|
+
} catch {
|
|
1958
|
+
printWarning("sharp not installed (montage command unavailable)");
|
|
1959
|
+
warnings++;
|
|
1960
|
+
}
|
|
1961
|
+
console.log("\n Client configurations:\n");
|
|
1962
|
+
for (const client of CLIENTS) {
|
|
1963
|
+
const installed = isInPath(client.binaryName);
|
|
1964
|
+
const configured = await isConfigured(client).catch(() => false);
|
|
1965
|
+
if (configured) {
|
|
1966
|
+
printCheck(true, `${client.name} \u2014 configured`);
|
|
1967
|
+
} else if (installed) {
|
|
1968
|
+
printWarning(
|
|
1969
|
+
`${client.name} \u2014 installed but not configured (run: genart agent install ${client.id})`
|
|
1970
|
+
);
|
|
1971
|
+
warnings++;
|
|
1972
|
+
} else {
|
|
1973
|
+
printNotInstalled(`${client.name} \u2014 not installed`);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
console.log("");
|
|
1977
|
+
process.exitCode = warnings > 0 ? 1 : 0;
|
|
1978
|
+
});
|
|
1979
|
+
function printCheck(pass, message) {
|
|
1980
|
+
const icon = pass ? chalk12.green("\u2713") : chalk12.red("\u2717");
|
|
1981
|
+
console.log(` ${icon} ${message}`);
|
|
1982
|
+
}
|
|
1983
|
+
function printWarning(message) {
|
|
1984
|
+
console.log(` ${chalk12.yellow("!")} ${message}`);
|
|
1985
|
+
}
|
|
1986
|
+
function printNotInstalled(message) {
|
|
1987
|
+
console.log(` ${chalk12.dim("\u25CB")} ${chalk12.dim(message)}`);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// src/commands/agent/index.ts
|
|
1991
|
+
var agentCommand = new Command15("agent").description("MCP server and agent configuration");
|
|
1992
|
+
agentCommand.addCommand(stdioCommand);
|
|
1993
|
+
agentCommand.addCommand(httpCommand);
|
|
1994
|
+
agentCommand.addCommand(sidecarCommand);
|
|
1995
|
+
agentCommand.addCommand(installCommand);
|
|
1996
|
+
agentCommand.addCommand(doctorCommand);
|
|
1997
|
+
|
|
1998
|
+
// src/index.ts
|
|
1999
|
+
var program = new Command16();
|
|
2000
|
+
program.name("genart").description("CLI for genart.dev \u2014 render, batch, montage, import, validate, export, and scaffold generative art sketches").version("0.1.0");
|
|
2001
|
+
program.addCommand(renderCommand);
|
|
2002
|
+
program.addCommand(infoCommand);
|
|
2003
|
+
program.addCommand(validateCommand);
|
|
2004
|
+
program.addCommand(initCommand);
|
|
2005
|
+
program.addCommand(exportCommand);
|
|
2006
|
+
program.addCommand(batchCommand);
|
|
2007
|
+
program.addCommand(montageCommand);
|
|
2008
|
+
program.addCommand(importCommand);
|
|
2009
|
+
program.addCommand(videoCommand);
|
|
2010
|
+
program.addCommand(agentCommand);
|
|
2011
|
+
program.parse();
|
|
2012
|
+
//# sourceMappingURL=index.js.map
|