@stdout-design/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander";
3
+ //#region src/lib/display.ts
4
+ const formatBytes = (bytes) => {
5
+ if (bytes < 1024) return `${bytes} B`;
6
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
7
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
8
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
9
+ };
10
+ const formatCacheStats = (stats) => {
11
+ return [
12
+ "",
13
+ "Cache Statistics",
14
+ "━━━━━━━━━━━━━━━━",
15
+ ` Location: ${stats.cacheDir}`,
16
+ ` Compiled: ${stats.compiled.entries} entries (in-memory compile cache)`,
17
+ ` Pixel: ${stats.pixels.entries} entries`,
18
+ ` Disk used: ${formatBytes(stats.pixels.sizeBytes)}`,
19
+ ` Disk limit: ${formatBytes(stats.pixels.maxSizeBytes)}`,
20
+ ""
21
+ ].join("\n");
22
+ };
23
+ const formatManifestSummary = (manifest) => {
24
+ const { completedCount, totalCount, succeeded, failed } = manifest;
25
+ const cacheHits = succeeded.filter((s) => s.cacheHit).length;
26
+ const lines = [
27
+ "",
28
+ "Batch Render Complete",
29
+ "━━━━━━━━━━━━━━━━━━━━",
30
+ ` Status: ${manifest.status}`,
31
+ ` Total: ${totalCount}`,
32
+ ` Completed: ${completedCount}`,
33
+ ` Cache: ${cacheHits} hits, ${succeeded.length - cacheHits} renders`,
34
+ ` Failed: ${failed.length}`,
35
+ ""
36
+ ];
37
+ if (succeeded.length > 0 && succeeded.length <= 10) {
38
+ lines.push("Output files:");
39
+ for (const entry of succeeded) lines.push(` ${entry.outputPath}`);
40
+ lines.push("");
41
+ }
42
+ if (failed.length > 0) {
43
+ lines.push("Failures:");
44
+ for (const f of failed) lines.push(` Row ${f.rowIndex}, ${f.locale}, ${f.preset}: ${f.error}`);
45
+ lines.push("");
46
+ }
47
+ return lines.join("\n");
48
+ };
49
+ const formatError = (error) => {
50
+ if (error instanceof Error) return `Error: ${error.message}`;
51
+ return `Error: ${String(error)}`;
52
+ };
53
+ const formatCacheCleanResult = (freedBytes) => `Cache cleared. Freed ${formatBytes(freedBytes)}.`;
54
+ //#endregion
55
+ //#region src/index.ts
56
+ const program = new Command().allowExcessArguments(true);
57
+ program.name("studio").description("Social media design tool — render, preview, batch").version("0.0.0");
58
+ program.command("dev").description("Start the studio dev server with live preview").argument("[rootDir]", "Project root directory (default: cwd)").option("-p, --port <number>", "Port to run on", "3000").option("--open", "Open browser on start").action(async (rootDir, options) => {
59
+ try {
60
+ const { dev } = await import("./dev-DoEMJ8Qi.mjs");
61
+ await dev(rootDir, options);
62
+ } catch (error) {
63
+ console.error(formatError(error));
64
+ process.exit(1);
65
+ }
66
+ });
67
+ program.command("render").description("Render a template — single or batch with --data").argument("<template>", "Template ID from studio.config.ts").argument("[props...]", "Props as key=value pairs (e.g. title=\"Hello\")").option("--data <path>", "CSV or JSON data file for batch rendering").option("--preset <ids>", "Comma-separated preset IDs").option("--locale <locales>", "Comma-separated locale codes").option("--out-dir <path>", "Output directory").option("--concurrency <n>", "Max concurrent renders in batch mode", "4").option("--fail-fast", "Stop batch on first error").option("--json", "Output results as JSON").action(async (template, props, options) => {
68
+ try {
69
+ const { render } = await import("./render-BhTeMXMX.mjs");
70
+ await render(template, props, options);
71
+ } catch (error) {
72
+ console.error(formatError(error));
73
+ process.exit(1);
74
+ }
75
+ });
76
+ const cacheCmd = program.command("cache").description("Manage the render cache");
77
+ cacheCmd.command("stats").description("Show cache statistics").option("--json", "Output as JSON").argument("[rootDir]", "Project root directory (default: cwd)").action(async (rootDir, options) => {
78
+ try {
79
+ const { cacheStats } = await import("./cache-BdS9VgtQ.mjs");
80
+ await cacheStats(rootDir, options);
81
+ } catch (error) {
82
+ console.error(formatError(error));
83
+ process.exit(1);
84
+ }
85
+ });
86
+ cacheCmd.command("clean").description("Clear the render cache").option("--json", "Output as JSON").argument("[rootDir]", "Project root directory (default: cwd)").action(async (rootDir, options) => {
87
+ try {
88
+ const { cacheClean } = await import("./cache-BdS9VgtQ.mjs");
89
+ await cacheClean(rootDir, options);
90
+ } catch (error) {
91
+ console.error(formatError(error));
92
+ process.exit(1);
93
+ }
94
+ });
95
+ program.command("lint").description("Check templates for compatibility (coming soon)").argument("[rootDir]", "Project root directory (default: cwd)").action(async (rootDir) => {
96
+ try {
97
+ const { lint } = await import("./lint-CacX6_Ob.mjs");
98
+ await lint(rootDir);
99
+ } catch (error) {
100
+ console.error(formatError(error));
101
+ process.exit(1);
102
+ }
103
+ });
104
+ program.command("init").description("Scaffold a new studio project").argument("[projectDir]", "Project directory (default: cwd)").option("-y, --yes", "Skip prompts, use defaults").action(async (projectDir, options) => {
105
+ try {
106
+ const { init } = await import("./init-CfgXE-J9.mjs");
107
+ await init(projectDir, { yes: options.yes ?? false });
108
+ } catch (error) {
109
+ console.error(formatError(error));
110
+ process.exit(1);
111
+ }
112
+ });
113
+ program.parse();
114
+ //#endregion
115
+ export { formatCacheStats as n, formatManifestSummary as r, formatCacheCleanResult as t };
@@ -0,0 +1,195 @@
1
+ import path from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ //#region src/commands/init.ts
5
+ const getConfigTemplate = () => `import type { StudioConfig } from "@stdout-design/cli";
6
+
7
+ const config: StudioConfig = {
8
+ defaultPreset: "instagram-square",
9
+ locales: ["en", "ar"],
10
+ outDir: "./out",
11
+ presets: [
12
+ { height: 1080, id: "instagram-square", platform: "instagram", width: 1080 },
13
+ { height: 1200, id: "x-card", platform: "x", width: 1200 },
14
+ ],
15
+ templates: {
16
+ "bento-feature": {
17
+ componentPath: "./templates/bento-feature",
18
+ description: "Apple-style bento feature card.",
19
+ },
20
+ },
21
+ };
22
+
23
+ export default config;
24
+ `;
25
+ const getBentoFeatureTemplate = () => `import { defineSchema } from "@stdout-design/cli/schema";
26
+ import { createElement, css, text } from "takumi-js";
27
+ import type { TakumiNode } from "takumi-js";
28
+
29
+ interface Props {
30
+ title?: string;
31
+ description?: string;
32
+ accent?: string;
33
+ }
34
+
35
+ export const propsSchema = defineSchema({
36
+ title: { type: "string", default: "Featured App", description: "Headline text" },
37
+ description: { type: "string", default: "A beautiful description goes here.", description: "Subtitle text" },
38
+ accent: { type: "string", default: "#6366f1", description: "Accent color" },
39
+ });
40
+
41
+ export default (props: Props): TakumiNode => {
42
+ const { title = "Featured App", description = "", accent = "#6366f1" } = props;
43
+
44
+ return createElement("div", {
45
+ style: css({
46
+ alignItems: "center",
47
+ background: "#0a0a0b",
48
+ borderRadius: 24,
49
+ display: "flex",
50
+ flexDirection: "column",
51
+ height: "100%",
52
+ justifyContent: "center",
53
+ padding: 48,
54
+ width: "100%",
55
+ }),
56
+ children: [
57
+ createElement("div", {
58
+ style: css({
59
+ background: accent,
60
+ borderRadius: 12,
61
+ height: 64,
62
+ marginBottom: 24,
63
+ width: 64,
64
+ }),
65
+ }),
66
+ createElement("h1", {
67
+ style: css({
68
+ color: "#ffffff",
69
+ fontFamily: "Inter",
70
+ fontSize: 42,
71
+ fontWeight: 700,
72
+ margin: 0,
73
+ textAlign: "center",
74
+ }),
75
+ children: [text(title)],
76
+ }),
77
+ description ? createElement("p", {
78
+ style: css({
79
+ color: "#a1a1aa",
80
+ fontFamily: "Inter",
81
+ fontSize: 18,
82
+ marginTop: 16,
83
+ textAlign: "center",
84
+ }),
85
+ children: [text(description)],
86
+ }) : null,
87
+ ],
88
+ });
89
+ };
90
+ `;
91
+ const scaffold = async (dir, options) => {
92
+ if (!existsSync(dir)) await mkdir(dir, { recursive: true });
93
+ const configPath = path.resolve(dir, "studio.config.ts");
94
+ if (existsSync(configPath)) {
95
+ console.log("studio.config.ts already exists — skipping scaffold.");
96
+ return;
97
+ }
98
+ if (options.templates.length > 0) await mkdir(path.resolve(dir, "templates"), { recursive: true });
99
+ if (options.locales.length > 0) await mkdir(path.resolve(dir, "locales"), { recursive: true });
100
+ await writeFile(configPath, getConfigTemplate());
101
+ const pkgJson = {
102
+ name: path.basename(dir),
103
+ private: true,
104
+ type: "module",
105
+ dependencies: { "takumi-js": "^2.0.2" }
106
+ };
107
+ await writeFile(path.resolve(dir, "package.json"), JSON.stringify(pkgJson, null, 2));
108
+ const writes = [];
109
+ for (const template of options.templates) if (template === "bento-feature") {
110
+ const templatesDir = path.resolve(dir, "templates");
111
+ writes.push(writeFile(path.resolve(templatesDir, "bento-feature.tsx"), getBentoFeatureTemplate()));
112
+ }
113
+ for (const locale of options.locales) {
114
+ const localesDir = path.resolve(dir, "locales");
115
+ if (locale === "en") writes.push(writeFile(path.resolve(localesDir, "en.json"), JSON.stringify({ title: "Featured App" }, null, 2)));
116
+ if (locale === "ar") writes.push(writeFile(path.resolve(localesDir, "ar.json"), JSON.stringify({ title: "التطبيق المميز" }, null, 2)));
117
+ }
118
+ await Promise.all(writes);
119
+ console.log(`Scaffolded studio project in ${dir}`);
120
+ console.log("");
121
+ console.log("Next steps:");
122
+ console.log(` cd ${dir}`);
123
+ console.log(" studio dev");
124
+ if (options.install) {
125
+ console.log("");
126
+ console.log("Installing dependencies...");
127
+ const { spawnSync } = await import("node:child_process");
128
+ spawnSync("bun", ["install"], {
129
+ cwd: dir,
130
+ stdio: "inherit"
131
+ });
132
+ }
133
+ };
134
+ const init = async (projectDir, options) => {
135
+ const dir = path.resolve(projectDir ?? process.cwd());
136
+ if (options?.yes) {
137
+ await scaffold(dir, {
138
+ install: true,
139
+ locales: ["en", "ar"],
140
+ templates: ["bento-feature"]
141
+ });
142
+ return;
143
+ }
144
+ const { intro, confirm, multiselect, outro, cancel, isCancel } = await import("@clack/prompts");
145
+ intro("Studio Init");
146
+ if (existsSync(path.resolve(dir, "studio.config.ts"))) {
147
+ console.log("studio.config.ts already exists — skipping scaffold.");
148
+ outro("Done");
149
+ return;
150
+ }
151
+ const selectedTemplates = await multiselect({
152
+ message: "Which templates would you like to scaffold?",
153
+ options: [{
154
+ hint: "Apple-style feature card",
155
+ label: "Bento Feature",
156
+ value: "bento-feature"
157
+ }]
158
+ });
159
+ if (isCancel(selectedTemplates)) {
160
+ cancel("Cancelled.");
161
+ process.exit(0);
162
+ }
163
+ const templates = selectedTemplates;
164
+ const selectedLocales = await multiselect({
165
+ message: "Which locales would you like to include?",
166
+ options: [{
167
+ label: "English",
168
+ value: "en"
169
+ }, {
170
+ label: "Arabic",
171
+ value: "ar"
172
+ }]
173
+ });
174
+ if (isCancel(selectedLocales)) {
175
+ cancel("Cancelled.");
176
+ process.exit(0);
177
+ }
178
+ const locales = selectedLocales;
179
+ const shouldInstall = await confirm({
180
+ initialValue: true,
181
+ message: "Install dependencies?"
182
+ });
183
+ if (isCancel(shouldInstall)) {
184
+ cancel("Cancelled.");
185
+ process.exit(0);
186
+ }
187
+ await scaffold(dir, {
188
+ install: shouldInstall,
189
+ locales,
190
+ templates
191
+ });
192
+ outro("Done");
193
+ };
194
+ //#endregion
195
+ export { init };
@@ -0,0 +1,7 @@
1
+ //#region src/commands/lint.ts
2
+ const lint = (_rootDir) => {
3
+ console.error("Lint command not yet implemented. Coming in a future release.");
4
+ process.exit(1);
5
+ };
6
+ //#endregion
7
+ export { lint };
@@ -0,0 +1,120 @@
1
+ import { r as formatManifestSummary } from "./index.mjs";
2
+ import { r as zodToJsonSchemaShape } from "./validation-CMTs74Vm-C3_d2dBv.mjs";
3
+ import { a as importTemplateForBatch, d as runBatch, n as ErrorCode, o as loadConfig, t as AppError } from "./dist-Dm76-R-V.mjs";
4
+ //#region src/lib/prop-parser.ts
5
+ const coerceValue = (value, type, _items) => {
6
+ switch (type) {
7
+ case "number": {
8
+ const n = Number(value);
9
+ return Number.isNaN(n) ? value : n;
10
+ }
11
+ case "boolean":
12
+ if (value === "true" || value === "1") return true;
13
+ if (value === "false" || value === "0") return false;
14
+ return value;
15
+ case "array":
16
+ if (value.startsWith("[") && value.endsWith("]")) try {
17
+ return JSON.parse(value);
18
+ } catch {
19
+ return value.split(",").map((s) => s.trim());
20
+ }
21
+ return value.split(",").map((s) => s.trim());
22
+ default: return value;
23
+ }
24
+ };
25
+ const parsePropArgs = (args, schema) => {
26
+ const props = {};
27
+ const properties = schema.properties ?? {};
28
+ for (const arg of args) {
29
+ const eqIndex = arg.indexOf("=");
30
+ if (eqIndex === -1) {
31
+ props[arg] = true;
32
+ continue;
33
+ }
34
+ const key = arg.slice(0, eqIndex);
35
+ const value = arg.slice(eqIndex + 1);
36
+ const field = properties[key];
37
+ props[key] = field ? coerceValue(value, field.type, field.items) : value;
38
+ }
39
+ return props;
40
+ };
41
+ const mergeDefaultProps = (schema, props) => {
42
+ const properties = schema.properties ?? {};
43
+ const merged = {};
44
+ for (const [key, field] of Object.entries(properties)) if (field.default !== void 0) merged[key] = field.default;
45
+ return {
46
+ ...merged,
47
+ ...props
48
+ };
49
+ };
50
+ //#endregion
51
+ //#region src/lib/suggest.ts
52
+ const levenshteinDistance = (a, b) => {
53
+ const aLen = a.length;
54
+ const bLen = b.length;
55
+ if (aLen === 0) return bLen;
56
+ if (bLen === 0) return aLen;
57
+ let prev = Array.from({ length: aLen + 1 }, (_, j) => j);
58
+ let curr = Array.from({ length: aLen + 1 }).fill(0);
59
+ for (let j = 1; j <= bLen; j += 1) {
60
+ curr[0] = j;
61
+ for (let i = 1; i <= aLen; i += 1) {
62
+ const cost = b[j - 1] === a[i - 1] ? 0 : 1;
63
+ curr[i] = Math.min((prev[i] ?? 0) + 1, (curr[i - 1] ?? 0) + 1, (prev[i - 1] ?? 0) + cost);
64
+ }
65
+ const tmp = prev;
66
+ prev = curr;
67
+ curr = tmp;
68
+ }
69
+ return prev[aLen] ?? 0;
70
+ };
71
+ const suggestClosest = (input, candidates, maxDistance = 3) => {
72
+ let best;
73
+ let bestDistance = Infinity;
74
+ for (const candidate of candidates) {
75
+ const dist = levenshteinDistance(input.toLowerCase(), candidate.toLowerCase());
76
+ if (dist < bestDistance && dist <= maxDistance) {
77
+ bestDistance = dist;
78
+ best = candidate;
79
+ }
80
+ }
81
+ return best;
82
+ };
83
+ //#endregion
84
+ //#region src/commands/render.ts
85
+ const render = async (templateId, propArgs, options) => {
86
+ const rootDir = process.cwd();
87
+ const config = await loadConfig(rootDir);
88
+ const templateEntry = config.templates[templateId];
89
+ if (!templateEntry) {
90
+ const suggestions = suggestClosest(templateId, Object.keys(config.templates));
91
+ const hint = suggestions ? ` Did you mean "${suggestions}"?` : "";
92
+ throw new AppError(ErrorCode.TEMPLATE_NOT_FOUND, `Template "${templateId}" not found in studio.config.ts.${hint}`, {
93
+ availableTemplates: Object.keys(config.templates),
94
+ templateId
95
+ });
96
+ }
97
+ const { module } = await importTemplateForBatch(rootDir, templateEntry.componentPath, templateId);
98
+ const schema = zodToJsonSchemaShape(module.propsSchema);
99
+ const mergedProps = mergeDefaultProps(schema, parsePropArgs(propArgs, schema));
100
+ const presetIds = options.preset ? options.preset.split(",").map((s) => s.trim()) : config.presets.map((p) => p.id);
101
+ const localeIds = options.locale ? options.locale.split(",").map((s) => s.trim()) : void 0;
102
+ const outDir = options.outDir ?? config.outDir ?? "out";
103
+ const { manifest } = await runBatch({
104
+ baseProps: mergedProps,
105
+ concurrency: Number(options.concurrency ?? "4"),
106
+ dataFile: options.data,
107
+ failFast: options.failFast,
108
+ locales: localeIds,
109
+ outDir,
110
+ presets: presetIds,
111
+ rootDir,
112
+ rows: options.data ? void 0 : [{}],
113
+ templateId
114
+ });
115
+ if (options.json) console.log(JSON.stringify(manifest, null, 2));
116
+ else console.log(formatManifestSummary(manifest));
117
+ if (manifest.failed.length > 0) process.exit(1);
118
+ };
119
+ //#endregion
120
+ export { render };
@@ -0,0 +1,2 @@
1
+ import { t as defineSchema } from "./validation-CMTs74Vm-C3_d2dBv.mjs";
2
+ export { defineSchema };
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ //#region ../core/dist/validation-CMTs74Vm.mjs
3
+ /** Identity helper purely for inference ergonomics at the template definition site. */
4
+ const defineSchema = (schema) => schema;
5
+ const isZodObject = (schema) => {
6
+ if (typeof schema !== "object" || schema === null) return false;
7
+ return schema.type === "object";
8
+ };
9
+ const zodToJsonSchemaShape = (schema) => {
10
+ if (typeof schema !== "object" || !schema || !("shape" in schema)) return {};
11
+ return z.toJSONSchema(schema);
12
+ };
13
+ //#endregion
14
+ export { isZodObject as n, zodToJsonSchemaShape as r, defineSchema as t };
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@stdout-design/cli",
3
+ "version": "0.1.0",
4
+ "description": "stdout-design CLI — shallow front door to @stdout-design/core and @stdout-design/dev-server",
5
+ "keywords": [
6
+ "cli",
7
+ "design",
8
+ "image-generation",
9
+ "social-media",
10
+ "takumi",
11
+ "tsx"
12
+ ],
13
+ "homepage": "https://github.com/stdout/studio#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/stdout/studio/issues"
16
+ },
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/stdout/studio.git"
21
+ },
22
+ "bin": {
23
+ "studio": "./dist/index.mjs"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "exports": {
30
+ ".": {
31
+ "development": {
32
+ "import": "./src/index.ts",
33
+ "types": "./src/index.ts"
34
+ },
35
+ "import": "./dist/index.mjs",
36
+ "types": "./dist/index.d.mts"
37
+ },
38
+ "./schema": {
39
+ "development": {
40
+ "import": "./src/schema.ts",
41
+ "types": "./src/schema.ts"
42
+ },
43
+ "import": "./dist/schema.mjs",
44
+ "types": "./dist/schema.d.mts"
45
+ }
46
+ },
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "dev": "tsdown --watch",
50
+ "check-types": "tsc --noEmit",
51
+ "lint": "ultracite check",
52
+ "test": "bun test"
53
+ },
54
+ "dependencies": {
55
+ "@clack/prompts": "^1.7.0",
56
+ "@takumi-rs/core": "^2.0.0",
57
+ "@takumi-rs/helpers": "^2.0.0",
58
+ "@vitejs/plugin-react": "^6.0.1",
59
+ "chokidar": "^5.0.0",
60
+ "commander": "^15.0.0",
61
+ "csv-parse": "^7.0.1",
62
+ "hono": "^4.12.26",
63
+ "kysely": "^0.29.3",
64
+ "kysely-bun-sqlite": "^0.4.0",
65
+ "ora": "^9.4.1",
66
+ "p-limit": "^7.0.0",
67
+ "proper-lockfile": "^4.1.2",
68
+ "react": "19.2.7",
69
+ "takumi-js": "^2.0.0",
70
+ "vite": "^8.0.12",
71
+ "vite-node": "^6.0.0",
72
+ "write-file-atomic": "^8.0.0",
73
+ "zod": "^4.4.3"
74
+ },
75
+ "devDependencies": {
76
+ "@stdout-design/config": "workspace:*",
77
+ "@stdout-design/core": "workspace:*",
78
+ "@stdout-design/dev-server": "workspace:*",
79
+ "@types/react": "19.2.17",
80
+ "tsdown": "^0.22.4",
81
+ "typescript": "^7"
82
+ }
83
+ }