@open-assets/open-assets 0.1.11 → 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/README.md +110 -120
- package/bin/open-assets.mjs +165 -319
- package/lib/add.mjs +476 -0
- package/lib/init.mjs +215 -107
- package/lib/list.mjs +64 -0
- package/lib/lockfile.mjs +53 -2
- package/lib/manifest.mjs +132 -0
- package/lib/render.mjs +219 -0
- package/lib/renderer.mjs +57 -7
- package/lib/server.mjs +58 -27
- package/lib/validate.mjs +93 -0
- package/package.json +7 -2
- package/public/img/banner/1280/banner.png +0 -0
- package/public/img/examples/cat-iq-test-icon.png +0 -0
- package/public/img/examples/cat-iq-test.png +0 -0
- package/public/img/examples/no-context-icon.png +0 -0
- package/public/img/examples/no-context.png +0 -0
- package/public/img/examples/only-recipes-icon.png +0 -0
- package/public/img/examples/only-recipes.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 +59 -56
- package/skills/open-assets/SKILL.md +121 -76
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);
|
|
@@ -68,18 +63,19 @@ program
|
|
|
68
63
|
|
|
69
64
|
const { startServer } = await import("../lib/server.mjs");
|
|
70
65
|
const port = parseInt(opts.port, 10);
|
|
71
|
-
startServer(projectDir, {
|
|
66
|
+
const { port: actualPort } = await startServer(projectDir, {
|
|
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
|
});
|
|
79
74
|
|
|
80
75
|
if (opts.open) {
|
|
81
76
|
const open = (await import("open")).default;
|
|
82
|
-
|
|
77
|
+
const displayHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
|
|
78
|
+
setTimeout(() => open(`http://${displayHost}:${actualPort}`), 500);
|
|
83
79
|
}
|
|
84
80
|
});
|
|
85
81
|
|
|
@@ -88,219 +84,65 @@ program
|
|
|
88
84
|
program
|
|
89
85
|
.command("render")
|
|
90
86
|
.description("Render assets headlessly via CLI")
|
|
91
|
-
.argument("[dir]", "Project directory containing
|
|
87
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
92
88
|
.option("--collection <id>", "Render only the collection with this ID")
|
|
93
89
|
.option("--tag <tag>", "Render only collections with this tag")
|
|
94
90
|
.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")
|
|
91
|
+
.option("--size <name>", "Use a named export size from config")
|
|
97
92
|
.option("--width <px>", "Output width in pixels (custom size)")
|
|
98
93
|
.option("--height <px>", "Output height in pixels (custom size)")
|
|
99
|
-
.option("--
|
|
100
|
-
.option("--force", "Re-render all assets even if unchanged")
|
|
94
|
+
.option("-f, --force", "Export at every size and re-render all assets (ignores cache)")
|
|
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,156 +150,124 @@ 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"))
|
|
192
|
+
.option("--fix", "Interactively fix issues (e.g. remove missing templates)")
|
|
381
193
|
.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
|
-
}
|
|
194
|
+
const { loadConfig, saveConfig } = await import("../lib/manifest.mjs");
|
|
195
|
+
const { validateConfig } = await import("../lib/validate.mjs");
|
|
389
196
|
|
|
197
|
+
const projectDir = resolve(dir);
|
|
390
198
|
let manifest;
|
|
391
199
|
try {
|
|
392
|
-
manifest =
|
|
200
|
+
manifest = loadConfig(projectDir, opts.config);
|
|
393
201
|
} catch (err) {
|
|
394
|
-
console.error(`Error:
|
|
202
|
+
console.error(`Error: ${err.message}`);
|
|
395
203
|
process.exit(1);
|
|
396
204
|
}
|
|
397
205
|
|
|
398
|
-
|
|
399
|
-
const ok = (msg) => console.log(` ✓ ${msg}`);
|
|
400
|
-
const fail = (msg) => { console.log(` ✗ ${msg}`); errors++; };
|
|
206
|
+
console.log(`\n Validating ${opts.config}...\n`);
|
|
401
207
|
|
|
402
|
-
|
|
208
|
+
const { checks, errors, missingTemplates, emptyCollections } = validateConfig(manifest, projectDir);
|
|
403
209
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
210
|
+
for (const check of checks) {
|
|
211
|
+
console.log(` ${check.ok ? "\u2713" : "\u2717"} ${check.message}`);
|
|
212
|
+
}
|
|
407
213
|
|
|
408
|
-
|
|
409
|
-
|
|
214
|
+
console.log();
|
|
215
|
+
if (errors === 0) {
|
|
216
|
+
console.log(" All checks passed.\n");
|
|
410
217
|
} else {
|
|
411
|
-
|
|
412
|
-
process.exit(1);
|
|
413
|
-
}
|
|
218
|
+
console.log(` ${errors} error(s) found.\n`);
|
|
414
219
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
220
|
+
const fixable = missingTemplates.length > 0 || emptyCollections.length > 0;
|
|
221
|
+
|
|
222
|
+
if (fixable && opts.fix) {
|
|
223
|
+
const { createInterface } = await import("readline");
|
|
224
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
225
|
+
const ask = (question) =>
|
|
226
|
+
new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim().toLowerCase())));
|
|
227
|
+
|
|
228
|
+
let changes = 0;
|
|
229
|
+
|
|
230
|
+
for (const missing of missingTemplates) {
|
|
231
|
+
const answer = await ask(
|
|
232
|
+
` Remove template "${missing.templateSrc}" from collection "${missing.collectionId}"? (y/N) `
|
|
233
|
+
);
|
|
234
|
+
if (answer === "y" || answer === "yes") {
|
|
235
|
+
const col = manifest.collections.find((c) => c.id === missing.collectionId);
|
|
236
|
+
if (col) {
|
|
237
|
+
col.templates = col.templates.filter((t) => t.src !== missing.templateSrc);
|
|
238
|
+
changes++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
427
242
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
fail(`Collection "${col.id}" has a template without 'src'`);
|
|
434
|
-
continue;
|
|
243
|
+
// Include collections that were already empty plus ones emptied by template removal
|
|
244
|
+
const emptyIds = new Set(emptyCollections.map((e) => e.collectionId));
|
|
245
|
+
for (const col of manifest.collections) {
|
|
246
|
+
if (!col.templates || col.templates.length === 0) {
|
|
247
|
+
emptyIds.add(col.id);
|
|
435
248
|
}
|
|
436
|
-
const filePath = resolve(projectDir, template.src);
|
|
437
|
-
if (existsSync(filePath)) ok(`${template.src} exists`);
|
|
438
|
-
else fail(`${template.src} not found`);
|
|
439
249
|
}
|
|
440
|
-
}
|
|
441
250
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
251
|
+
for (const collectionId of emptyIds) {
|
|
252
|
+
const answer = await ask(
|
|
253
|
+
` Remove empty collection "${collectionId}"? (y/N) `
|
|
254
|
+
);
|
|
255
|
+
if (answer === "y" || answer === "yes") {
|
|
256
|
+
manifest.collections = manifest.collections.filter((c) => c.id !== collectionId);
|
|
257
|
+
changes++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
446
260
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
261
|
+
rl.close();
|
|
262
|
+
|
|
263
|
+
if (changes > 0) {
|
|
264
|
+
saveConfig(projectDir, opts.config, manifest);
|
|
265
|
+
console.log(`\n Updated ${opts.config} (${changes} fix(es) applied).\n`);
|
|
452
266
|
}
|
|
267
|
+
} else if (fixable) {
|
|
268
|
+
console.log(` Run with --fix to remove missing templates and empty collections.\n`);
|
|
453
269
|
}
|
|
454
|
-
}
|
|
455
270
|
|
|
456
|
-
console.log();
|
|
457
|
-
if (errors === 0) {
|
|
458
|
-
console.log(" All checks passed.\n");
|
|
459
|
-
} else {
|
|
460
|
-
console.log(` ${errors} error(s) found.\n`);
|
|
461
271
|
process.exit(1);
|
|
462
272
|
}
|
|
463
273
|
});
|
|
@@ -466,13 +276,49 @@ program
|
|
|
466
276
|
|
|
467
277
|
program
|
|
468
278
|
.command("init")
|
|
469
|
-
.description("Scaffold a new
|
|
279
|
+
.description("Scaffold a new assets.json and example assets")
|
|
470
280
|
.argument("[dir]", "Target directory", ".")
|
|
471
281
|
.action(async (dir) => {
|
|
472
282
|
const { scaffoldProject } = await import("../lib/init.mjs");
|
|
473
283
|
await scaffoldProject(resolve(dir));
|
|
474
284
|
});
|
|
475
285
|
|
|
286
|
+
// ── add ─────────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
const add = program
|
|
289
|
+
.command("add")
|
|
290
|
+
.description("Add collections, templates, or export sizes to the config");
|
|
291
|
+
|
|
292
|
+
add
|
|
293
|
+
.command("collection")
|
|
294
|
+
.description("Add a new asset collection (choose from presets or custom)")
|
|
295
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
296
|
+
.option("--config <path>", "Path to config file", env("OPEN_ASSETS_CONFIG", "assets.json"))
|
|
297
|
+
.action(async (dir, opts) => {
|
|
298
|
+
const { addCollection } = await import("../lib/add.mjs");
|
|
299
|
+
await addCollection(resolve(dir), opts.config);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
add
|
|
303
|
+
.command("template")
|
|
304
|
+
.description("Add a new template to an existing collection (HTML or SVG)")
|
|
305
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
306
|
+
.option("--config <path>", "Path to config file", env("OPEN_ASSETS_CONFIG", "assets.json"))
|
|
307
|
+
.action(async (dir, opts) => {
|
|
308
|
+
const { addTemplate } = await import("../lib/add.mjs");
|
|
309
|
+
await addTemplate(resolve(dir), opts.config);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
add
|
|
313
|
+
.command("size")
|
|
314
|
+
.description("Add an export size to a collection (choose from presets or custom)")
|
|
315
|
+
.argument("[dir]", "Project directory containing assets.json", ".")
|
|
316
|
+
.option("--config <path>", "Path to config file", env("OPEN_ASSETS_CONFIG", "assets.json"))
|
|
317
|
+
.action(async (dir, opts) => {
|
|
318
|
+
const { addSize } = await import("../lib/add.mjs");
|
|
319
|
+
await addSize(resolve(dir), opts.config);
|
|
320
|
+
});
|
|
321
|
+
|
|
476
322
|
// ── skills ──────────────────────────────────────────────────────────────────
|
|
477
323
|
|
|
478
324
|
program
|