@open-assets/open-assets 0.1.11 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -88
- package/bin/open-assets.mjs +113 -320
- package/lib/add.mjs +476 -0
- package/lib/init.mjs +215 -107
- package/lib/list.mjs +66 -0
- package/lib/lockfile.mjs +1 -1
- package/lib/manifest.mjs +125 -0
- package/lib/render.mjs +206 -0
- package/lib/renderer.mjs +19 -5
- package/lib/server.mjs +14 -4
- package/lib/validate.mjs +90 -0
- package/package.json +7 -2
- package/public/img/banner/1280/banner.png +0 -0
- package/public/img/icon/16/icon.png +0 -0
- package/public/img/icon/180/icon.png +0 -0
- package/public/img/icon/192/icon.png +0 -0
- package/public/img/icon/32/icon.png +0 -0
- package/public/img/icon/512/icon.png +0 -0
- package/public/viewer.html +28 -25
- package/skills/open-assets/SKILL.md +99 -52
package/bin/open-assets.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { resolve, join } from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Resolve an option value with env var fallback.
|
|
@@ -11,13 +11,6 @@ function env(name, fallback) {
|
|
|
11
11
|
return process.env[name] ?? fallback;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* Get all export sizes from a collection, flattened.
|
|
16
|
-
*/
|
|
17
|
-
function flatSizes(col) {
|
|
18
|
-
return (col.exportSizes || []).flatMap((group) => group.sizes || []);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
14
|
const program = new Command();
|
|
22
15
|
|
|
23
16
|
const pkg = JSON.parse(
|
|
@@ -34,22 +27,24 @@ program
|
|
|
34
27
|
program
|
|
35
28
|
.command("dev")
|
|
36
29
|
.description("Start the dev server with live preview and export UI")
|
|
37
|
-
.argument("[dir]", "Project directory containing
|
|
30
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
38
31
|
.option("-p, --port <port>", "Port to listen on (env: OPEN_ASSETS_PORT)", env("OPEN_ASSETS_PORT", "3200"))
|
|
39
32
|
.option("-H, --host <host>", "Host to bind to (env: OPEN_ASSETS_HOST)", env("OPEN_ASSETS_HOST", "localhost"))
|
|
40
33
|
.option("--no-open", "Don't auto-open the browser (env: OPEN_ASSETS_NO_OPEN)")
|
|
41
34
|
.option("-q, --quiet", "Suppress server logs (env: OPEN_ASSETS_QUIET)", env("OPEN_ASSETS_QUIET") === "true")
|
|
42
35
|
.option("--ci", "CI mode: no browser, quiet logging", env("CI") === "true")
|
|
43
|
-
.option("--
|
|
36
|
+
.option("--config <path>", "Path to config file (env: OPEN_ASSETS_CONFIG)", env("OPEN_ASSETS_CONFIG", "assets.json"))
|
|
44
37
|
.option("--static-dir <dirs...>", "Additional static directories to serve")
|
|
45
38
|
.option("--render-timeout <ms>", "Puppeteer render timeout in ms (env: OPEN_ASSETS_RENDER_TIMEOUT)", env("OPEN_ASSETS_RENDER_TIMEOUT", "30000"))
|
|
46
39
|
.action(async (dir, opts) => {
|
|
40
|
+
const { loadConfig } = await import("../lib/manifest.mjs");
|
|
47
41
|
const projectDir = resolve(dir);
|
|
48
|
-
const manifestPath = resolve(projectDir, opts.manifest);
|
|
49
42
|
|
|
50
|
-
|
|
43
|
+
try {
|
|
44
|
+
loadConfig(projectDir, opts.config);
|
|
45
|
+
} catch {
|
|
51
46
|
console.error(
|
|
52
|
-
`Error: ${opts.
|
|
47
|
+
`Error: ${opts.config} not found in ${projectDir}\n` +
|
|
53
48
|
`Run "open-assets init" to create one.`
|
|
54
49
|
);
|
|
55
50
|
process.exit(1);
|
|
@@ -72,7 +67,7 @@ program
|
|
|
72
67
|
port,
|
|
73
68
|
host: opts.host,
|
|
74
69
|
quiet: opts.quiet,
|
|
75
|
-
manifestPath,
|
|
70
|
+
manifestPath: resolve(projectDir, opts.config),
|
|
76
71
|
staticDirs: opts.staticDir,
|
|
77
72
|
renderTimeout: parseInt(opts.renderTimeout, 10),
|
|
78
73
|
});
|
|
@@ -88,219 +83,66 @@ program
|
|
|
88
83
|
program
|
|
89
84
|
.command("render")
|
|
90
85
|
.description("Render assets headlessly via CLI")
|
|
91
|
-
.argument("[dir]", "Project directory containing
|
|
86
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
92
87
|
.option("--collection <id>", "Render only the collection with this ID")
|
|
93
88
|
.option("--tag <tag>", "Render only collections with this tag")
|
|
94
89
|
.option("--template <name>", "Render only the template with this name")
|
|
95
|
-
.option("--size <name>", "Use a named export size from
|
|
96
|
-
.option("--platform <name>", "Render only sizes for this platform")
|
|
90
|
+
.option("--size <name>", "Use a named export size from config")
|
|
97
91
|
.option("--width <px>", "Output width in pixels (custom size)")
|
|
98
92
|
.option("--height <px>", "Output height in pixels (custom size)")
|
|
99
|
-
.option("--all", "Export at every size defined in the
|
|
93
|
+
.option("--all", "Export at every size defined in the config")
|
|
100
94
|
.option("--force", "Re-render all assets even if unchanged")
|
|
101
95
|
.option("-o, --output <dir>", "Output directory (env: OPEN_ASSETS_OUTPUT)", env("OPEN_ASSETS_OUTPUT", "./exports"))
|
|
102
|
-
.option("--
|
|
96
|
+
.option("--config <path>", "Path to config file (env: OPEN_ASSETS_CONFIG)", env("OPEN_ASSETS_CONFIG", "assets.json"))
|
|
103
97
|
.option("--parallel <count>", "Number of parallel renders (env: OPEN_ASSETS_PARALLEL)", env("OPEN_ASSETS_PARALLEL", "1"))
|
|
104
98
|
.option("--render-timeout <ms>", "Puppeteer render timeout in ms", env("OPEN_ASSETS_RENDER_TIMEOUT", "30000"))
|
|
105
99
|
.option("--json", "Output results as JSON (for CI/scripting)")
|
|
106
100
|
.option("-q, --quiet", "Suppress progress logs", env("OPEN_ASSETS_QUIET") === "true")
|
|
107
101
|
.action(async (dir, opts) => {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
102
|
+
const { loadConfig } = await import("../lib/manifest.mjs");
|
|
103
|
+
const { renderAssets } = await import("../lib/render.mjs");
|
|
104
|
+
const renderer = await import("../lib/renderer.mjs");
|
|
105
|
+
const lockfile = await import("../lib/lockfile.mjs");
|
|
110
106
|
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
const projectDir = resolve(dir);
|
|
108
|
+
let manifest;
|
|
109
|
+
try {
|
|
110
|
+
manifest = loadConfig(projectDir, opts.config);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`Error: ${err.message}`);
|
|
113
113
|
process.exit(1);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
if (opts.tag) {
|
|
134
|
-
collections = collections.filter((c) => (c.tags || []).includes(opts.tag));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (collections.length === 0) {
|
|
138
|
-
const filter = opts.collection ? `collection "${opts.collection}"` : `tag "${opts.tag}"`;
|
|
139
|
-
console.error(`Error: no collections matched ${filter} in manifest`);
|
|
116
|
+
const log = opts.quiet ? undefined : (msg) => console.log(msg);
|
|
117
|
+
const deps = {
|
|
118
|
+
renderScreenshot: renderer.renderScreenshot,
|
|
119
|
+
runXcodeOutput: renderer.runXcodeOutput,
|
|
120
|
+
closeBrowser: renderer.closeBrowser,
|
|
121
|
+
readLockfile: lockfile.readLockfile,
|
|
122
|
+
writeLockfile: lockfile.writeLockfile,
|
|
123
|
+
computeChecksum: lockfile.computeChecksum,
|
|
124
|
+
isUpToDate: lockfile.isUpToDate,
|
|
125
|
+
recordExport: lockfile.recordExport,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const result = await renderAssets(projectDir, manifest, opts, deps, log);
|
|
129
|
+
|
|
130
|
+
if (result.error) {
|
|
131
|
+
console.error(`Error: ${result.error}`);
|
|
140
132
|
process.exit(1);
|
|
141
133
|
}
|
|
142
134
|
|
|
143
|
-
const lockData = readLockfile(projectDir);
|
|
144
|
-
const results = [];
|
|
145
|
-
let skipped = 0;
|
|
146
|
-
const startTime = Date.now();
|
|
147
|
-
|
|
148
|
-
for (const col of collections) {
|
|
149
|
-
const sourceW = col.sourceSize.width;
|
|
150
|
-
const sourceH = col.sourceSize.height;
|
|
151
|
-
|
|
152
|
-
// Filter templates
|
|
153
|
-
const templates = opts.template
|
|
154
|
-
? col.templates.filter((t) => t.name === opts.template || t.label === opts.template)
|
|
155
|
-
: col.templates;
|
|
156
|
-
|
|
157
|
-
// Determine which sizes to render
|
|
158
|
-
let sizesToRender = [];
|
|
159
|
-
|
|
160
|
-
if (opts.width && opts.height) {
|
|
161
|
-
// Custom size from CLI
|
|
162
|
-
sizesToRender = [{
|
|
163
|
-
name: `${opts.width}x${opts.height}`,
|
|
164
|
-
label: `${opts.width}x${opts.height}`,
|
|
165
|
-
width: parseInt(opts.width),
|
|
166
|
-
height: parseInt(opts.height),
|
|
167
|
-
}];
|
|
168
|
-
} else if (opts.size) {
|
|
169
|
-
// Specific named size
|
|
170
|
-
const found = flatSizes(col).find((s) => s.name === opts.size || s.label === opts.size);
|
|
171
|
-
if (found) {
|
|
172
|
-
sizesToRender = [found];
|
|
173
|
-
} else {
|
|
174
|
-
if (!opts.quiet) console.log(` Warning: size "${opts.size}" not found in collection "${col.id}", skipping`);
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
} else if (opts.all) {
|
|
178
|
-
// All sizes
|
|
179
|
-
sizesToRender = flatSizes(col);
|
|
180
|
-
// Filter by platform if specified
|
|
181
|
-
if (opts.platform) {
|
|
182
|
-
sizesToRender = (col.exportSizes || [])
|
|
183
|
-
.filter((g) => g.platform && g.platform.toLowerCase().includes(opts.platform.toLowerCase()))
|
|
184
|
-
.flatMap((g) => g.sizes || []);
|
|
185
|
-
}
|
|
186
|
-
} else {
|
|
187
|
-
// Default: render at source size
|
|
188
|
-
sizesToRender = [{
|
|
189
|
-
name: `${sourceW}x${sourceH}`,
|
|
190
|
-
label: `${sourceW}x${sourceH}`,
|
|
191
|
-
width: sourceW,
|
|
192
|
-
height: sourceH,
|
|
193
|
-
}];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (sizesToRender.length === 0) {
|
|
197
|
-
if (!opts.quiet) console.log(` No export sizes found for collection "${col.id}"`);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Determine if we need subdirectories (multiple sizes = subdirs)
|
|
202
|
-
const useSubDirs = sizesToRender.length > 1 || opts.all;
|
|
203
|
-
|
|
204
|
-
for (const size of sizesToRender) {
|
|
205
|
-
const sizeDir = useSubDirs
|
|
206
|
-
? join(outputDir, col.id, size.name)
|
|
207
|
-
: join(outputDir, col.id);
|
|
208
|
-
mkdirSync(sizeDir, { recursive: true });
|
|
209
|
-
|
|
210
|
-
if (!opts.quiet && sizesToRender.length > 1) {
|
|
211
|
-
const platformInfo = (col.exportSizes || [])
|
|
212
|
-
.find((g) => (g.sizes || []).some((s) => s.name === size.name));
|
|
213
|
-
const platform = platformInfo?.platform ? `[${platformInfo.platform}] ` : '';
|
|
214
|
-
console.log(`\n ${platform}${size.label} (${size.width}x${size.height})`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
for (const template of templates) {
|
|
218
|
-
const outPath = join(sizeDir, `${template.name}.png`);
|
|
219
|
-
const assetKey = `${col.id}/${template.name}`;
|
|
220
|
-
const variantKey = size.name;
|
|
221
|
-
const srcPath = resolve(projectDir, template.src);
|
|
222
|
-
const checksum = computeChecksum(srcPath);
|
|
223
|
-
|
|
224
|
-
// Skip if unchanged
|
|
225
|
-
if (!opts.force && checksum && isUpToDate(lockData, assetKey, variantKey, checksum, outPath)) {
|
|
226
|
-
if (!opts.quiet) console.log(` Skipping ${template.name} at ${size.width}x${size.height} (unchanged)`);
|
|
227
|
-
skipped++;
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (!opts.quiet) console.log(` Rendering ${template.name} at ${size.width}x${size.height}...`);
|
|
232
|
-
|
|
233
|
-
// Detect SVG source → use copy-source approach for SVG outputs
|
|
234
|
-
const isSvg = template.src.endsWith(".svg");
|
|
235
|
-
|
|
236
|
-
if (isSvg && size.width === sourceW && size.height === sourceH) {
|
|
237
|
-
// For SVGs at source size, just copy the source
|
|
238
|
-
copyFileSync(srcPath, outPath.replace(".png", ".svg"));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const buffer = await renderScreenshot(
|
|
242
|
-
projectDir,
|
|
243
|
-
template.src,
|
|
244
|
-
size.width,
|
|
245
|
-
size.height,
|
|
246
|
-
sourceW,
|
|
247
|
-
sourceH
|
|
248
|
-
);
|
|
249
|
-
writeFileSync(outPath, buffer);
|
|
250
|
-
results.push({
|
|
251
|
-
collection: col.id,
|
|
252
|
-
template: template.name,
|
|
253
|
-
path: outPath,
|
|
254
|
-
width: size.width,
|
|
255
|
-
height: size.height,
|
|
256
|
-
size: buffer.length,
|
|
257
|
-
});
|
|
258
|
-
if (checksum) recordExport(lockData, assetKey, variantKey, checksum, outPath);
|
|
259
|
-
if (!opts.quiet) console.log(` → ${outPath}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Handle outputs (xcode, copy-source, etc.)
|
|
264
|
-
if (col.outputs && opts.all) {
|
|
265
|
-
for (const output of col.outputs) {
|
|
266
|
-
if (output.type === "xcode") {
|
|
267
|
-
if (!opts.quiet) console.log(`\n Exporting to Xcode: ${output.path}`);
|
|
268
|
-
const path = await runXcodeOutput(projectDir, col, output);
|
|
269
|
-
results.push({ collection: col.id, type: "xcode", path });
|
|
270
|
-
} else if (output.type === "copy-source") {
|
|
271
|
-
// Copy source files (e.g., SVG originals)
|
|
272
|
-
const format = output.format || "svg";
|
|
273
|
-
const copyDir = join(outputDir, col.id, format);
|
|
274
|
-
mkdirSync(copyDir, { recursive: true });
|
|
275
|
-
for (const template of templates) {
|
|
276
|
-
if (template.src.endsWith(`.${format}`)) {
|
|
277
|
-
const srcPath = resolve(projectDir, template.src);
|
|
278
|
-
const destPath = join(copyDir, `${template.name}.${format}`);
|
|
279
|
-
if (existsSync(srcPath)) {
|
|
280
|
-
copyFileSync(srcPath, destPath);
|
|
281
|
-
results.push({ collection: col.id, template: template.name, type: `copy-${format}`, path: destPath });
|
|
282
|
-
if (!opts.quiet) console.log(` → ${destPath}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Persist lockfile
|
|
292
|
-
writeLockfile(projectDir, lockData);
|
|
293
|
-
|
|
294
|
-
await closeBrowser();
|
|
295
|
-
|
|
296
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
297
|
-
|
|
298
135
|
if (opts.json) {
|
|
299
|
-
console.log(JSON.stringify({
|
|
136
|
+
console.log(JSON.stringify({
|
|
137
|
+
ok: true,
|
|
138
|
+
assets: result.results,
|
|
139
|
+
skipped: result.skipped,
|
|
140
|
+
elapsed: result.elapsed,
|
|
141
|
+
}, null, 2));
|
|
300
142
|
} else if (!opts.quiet) {
|
|
301
|
-
const parts = [`${results.length} asset(s) rendered`];
|
|
302
|
-
if (skipped > 0) parts.push(`${skipped} skipped (unchanged)`);
|
|
303
|
-
console.log(`\nDone. ${parts.join(", ")} in ${elapsed}
|
|
143
|
+
const parts = [`${result.results.length} asset(s) rendered`];
|
|
144
|
+
if (result.skipped > 0) parts.push(`${result.skipped} skipped (unchanged)`);
|
|
145
|
+
console.log(`\nDone. ${parts.join(", ")} in ${result.elapsed}.`);
|
|
304
146
|
}
|
|
305
147
|
});
|
|
306
148
|
|
|
@@ -308,149 +150,64 @@ program
|
|
|
308
150
|
|
|
309
151
|
program
|
|
310
152
|
.command("list")
|
|
311
|
-
.description("List all collections and templates defined in the
|
|
312
|
-
.argument("[dir]", "Project directory containing
|
|
313
|
-
.option("--
|
|
153
|
+
.description("List all collections and templates defined in the config")
|
|
154
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
155
|
+
.option("--config <path>", "Path to config file", env("OPEN_ASSETS_CONFIG", "assets.json"))
|
|
314
156
|
.option("--tag <tag>", "List only collections with this tag")
|
|
315
157
|
.option("--json", "Output as JSON")
|
|
316
158
|
.action(async (dir, opts) => {
|
|
317
|
-
const
|
|
318
|
-
const
|
|
159
|
+
const { loadConfig } = await import("../lib/manifest.mjs");
|
|
160
|
+
const { listCollections, formatCollectionList } = await import("../lib/list.mjs");
|
|
319
161
|
|
|
320
|
-
|
|
321
|
-
|
|
162
|
+
const projectDir = resolve(dir);
|
|
163
|
+
let manifest;
|
|
164
|
+
try {
|
|
165
|
+
manifest = loadConfig(projectDir, opts.config);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error(`Error: ${err.message}`);
|
|
322
168
|
process.exit(1);
|
|
323
169
|
}
|
|
324
170
|
|
|
325
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
326
|
-
|
|
327
171
|
if (opts.json) {
|
|
328
172
|
console.log(JSON.stringify(manifest, null, 2));
|
|
329
173
|
return;
|
|
330
174
|
}
|
|
331
175
|
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
console.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
let cols = manifest.collections;
|
|
338
|
-
if (opts.tag) {
|
|
339
|
-
cols = cols.filter((c) => (c.tags || []).includes(opts.tag));
|
|
340
|
-
if (cols.length === 0) {
|
|
341
|
-
console.error(`\n No collections found with tag "${opts.tag}"\n`);
|
|
342
|
-
process.exit(1);
|
|
343
|
-
}
|
|
176
|
+
const result = listCollections(manifest, { tag: opts.tag });
|
|
177
|
+
if (result.error) {
|
|
178
|
+
console.error(`\n ${result.error}\n`);
|
|
179
|
+
process.exit(1);
|
|
344
180
|
}
|
|
345
181
|
|
|
346
|
-
console.log(`\n ${manifest.
|
|
347
|
-
for (const col of cols) {
|
|
348
|
-
const templateCount = col.templates.length;
|
|
349
|
-
const sizeCount = flatSizes(col).length;
|
|
350
|
-
const tagStr = (col.tags && col.tags.length > 0) ? ` [${col.tags.join(", ")}]` : "";
|
|
351
|
-
console.log(` ${col.label} (${col.id})${tagStr} — ${templateCount} template(s), ${sizeCount} size(s)`);
|
|
352
|
-
|
|
353
|
-
for (const template of col.templates) {
|
|
354
|
-
console.log(` • ${template.label || template.name} → ${template.src}`);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
for (const group of col.exportSizes || []) {
|
|
358
|
-
for (const size of group.sizes || []) {
|
|
359
|
-
const platform = group.platform ? `${group.platform}: ` : '';
|
|
360
|
-
console.log(` ↳ ${platform}${size.label} (${size.width}×${size.height})`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (col.outputs) {
|
|
365
|
-
for (const output of col.outputs) {
|
|
366
|
-
if (output.type === "xcode") console.log(` ↳ Xcode: ${output.path}`);
|
|
367
|
-
else if (output.type === "copy-source") console.log(` ↳ Copy source: ${output.format}`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
console.log();
|
|
182
|
+
console.log(`\n ${formatCollectionList(manifest, result.collections).split("\n").join("\n ")}\n`);
|
|
372
183
|
});
|
|
373
184
|
|
|
374
185
|
// ── validate ─────────────────────────────────────────────────────────────────
|
|
375
186
|
|
|
376
187
|
program
|
|
377
188
|
.command("validate")
|
|
378
|
-
.description("Validate the
|
|
379
|
-
.argument("[dir]", "Project directory containing
|
|
380
|
-
.option("--
|
|
189
|
+
.description("Validate the assets.json and check that all referenced files exist")
|
|
190
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
191
|
+
.option("--config <path>", "Path to config file", env("OPEN_ASSETS_CONFIG", "assets.json"))
|
|
381
192
|
.action(async (dir, opts) => {
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
if (!existsSync(manifestPath)) {
|
|
386
|
-
console.error(`Error: ${opts.manifest} not found in ${projectDir}`);
|
|
387
|
-
process.exit(1);
|
|
388
|
-
}
|
|
193
|
+
const { loadConfig } = await import("../lib/manifest.mjs");
|
|
194
|
+
const { validateConfig } = await import("../lib/validate.mjs");
|
|
389
195
|
|
|
196
|
+
const projectDir = resolve(dir);
|
|
390
197
|
let manifest;
|
|
391
198
|
try {
|
|
392
|
-
manifest =
|
|
199
|
+
manifest = loadConfig(projectDir, opts.config);
|
|
393
200
|
} catch (err) {
|
|
394
|
-
console.error(`Error:
|
|
201
|
+
console.error(`Error: ${err.message}`);
|
|
395
202
|
process.exit(1);
|
|
396
203
|
}
|
|
397
204
|
|
|
398
|
-
|
|
399
|
-
const ok = (msg) => console.log(` ✓ ${msg}`);
|
|
400
|
-
const fail = (msg) => { console.log(` ✗ ${msg}`); errors++; };
|
|
205
|
+
console.log(`\n Validating ${opts.config}...\n`);
|
|
401
206
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// Check top-level fields
|
|
405
|
-
if (manifest.name) ok(`name: "${manifest.name}"`);
|
|
406
|
-
else fail("Missing top-level 'name' field");
|
|
407
|
-
|
|
408
|
-
if (Array.isArray(manifest.collections) && manifest.collections.length > 0) {
|
|
409
|
-
ok(`${manifest.collections.length} collection(s) defined`);
|
|
410
|
-
} else {
|
|
411
|
-
fail("Missing or empty 'collections' array");
|
|
412
|
-
process.exit(1);
|
|
413
|
-
}
|
|
207
|
+
const { checks, errors } = validateConfig(manifest, projectDir);
|
|
414
208
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
for (const col of manifest.collections) {
|
|
418
|
-
if (!col.id) { fail(`Collection missing 'id' field`); continue; }
|
|
419
|
-
if (ids.has(col.id)) { fail(`Duplicate collection id: "${col.id}"`); }
|
|
420
|
-
ids.add(col.id);
|
|
421
|
-
|
|
422
|
-
if (!col.sourceSize || !col.sourceSize.width || !col.sourceSize.height) {
|
|
423
|
-
fail(`Collection "${col.id}" missing or invalid 'sourceSize'`);
|
|
424
|
-
} else {
|
|
425
|
-
ok(`Collection "${col.id}" sourceSize: ${col.sourceSize.width}×${col.sourceSize.height}`);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (!col.templates || col.templates.length === 0) {
|
|
429
|
-
fail(`Collection "${col.id}" has no templates`);
|
|
430
|
-
} else {
|
|
431
|
-
for (const template of col.templates) {
|
|
432
|
-
if (!template.src) {
|
|
433
|
-
fail(`Collection "${col.id}" has a template without 'src'`);
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
const filePath = resolve(projectDir, template.src);
|
|
437
|
-
if (existsSync(filePath)) ok(`${template.src} exists`);
|
|
438
|
-
else fail(`${template.src} not found`);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const sizes = flatSizes(col);
|
|
443
|
-
if (sizes.length > 0) {
|
|
444
|
-
ok(`Collection "${col.id}" has ${sizes.length} export size(s)`);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Validate outputs
|
|
448
|
-
if (col.outputs) {
|
|
449
|
-
for (const output of col.outputs) {
|
|
450
|
-
if (!output.type) fail(`Collection "${col.id}" has an output without 'type'`);
|
|
451
|
-
else ok(`Output: ${output.type}`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
209
|
+
for (const check of checks) {
|
|
210
|
+
console.log(` ${check.ok ? "\u2713" : "\u2717"} ${check.message}`);
|
|
454
211
|
}
|
|
455
212
|
|
|
456
213
|
console.log();
|
|
@@ -466,13 +223,49 @@ program
|
|
|
466
223
|
|
|
467
224
|
program
|
|
468
225
|
.command("init")
|
|
469
|
-
.description("Scaffold a new
|
|
226
|
+
.description("Scaffold a new assets.json and example assets")
|
|
470
227
|
.argument("[dir]", "Target directory", ".")
|
|
471
228
|
.action(async (dir) => {
|
|
472
229
|
const { scaffoldProject } = await import("../lib/init.mjs");
|
|
473
230
|
await scaffoldProject(resolve(dir));
|
|
474
231
|
});
|
|
475
232
|
|
|
233
|
+
// ── add ─────────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
const add = program
|
|
236
|
+
.command("add")
|
|
237
|
+
.description("Add collections, templates, or export sizes to the config");
|
|
238
|
+
|
|
239
|
+
add
|
|
240
|
+
.command("collection")
|
|
241
|
+
.description("Add a new asset collection (choose from presets or custom)")
|
|
242
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
243
|
+
.option("--config <path>", "Path to config file", env("OPEN_ASSETS_CONFIG", "assets.json"))
|
|
244
|
+
.action(async (dir, opts) => {
|
|
245
|
+
const { addCollection } = await import("../lib/add.mjs");
|
|
246
|
+
await addCollection(resolve(dir), opts.config);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
add
|
|
250
|
+
.command("template")
|
|
251
|
+
.description("Add a new template to an existing collection (HTML or SVG)")
|
|
252
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
253
|
+
.option("--config <path>", "Path to config file", env("OPEN_ASSETS_CONFIG", "assets.json"))
|
|
254
|
+
.action(async (dir, opts) => {
|
|
255
|
+
const { addTemplate } = await import("../lib/add.mjs");
|
|
256
|
+
await addTemplate(resolve(dir), opts.config);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
add
|
|
260
|
+
.command("size")
|
|
261
|
+
.description("Add an export size to a collection (choose from presets or custom)")
|
|
262
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
263
|
+
.option("--config <path>", "Path to config file", env("OPEN_ASSETS_CONFIG", "assets.json"))
|
|
264
|
+
.action(async (dir, opts) => {
|
|
265
|
+
const { addSize } = await import("../lib/add.mjs");
|
|
266
|
+
await addSize(resolve(dir), opts.config);
|
|
267
|
+
});
|
|
268
|
+
|
|
476
269
|
// ── skills ──────────────────────────────────────────────────────────────────
|
|
477
270
|
|
|
478
271
|
program
|