svg-terminal 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,524 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BlockConfigError,
4
+ ConfigError,
5
+ generate,
6
+ generateStatic,
7
+ getBlock,
8
+ inspectCache,
9
+ listBlocks,
10
+ loadConfig,
11
+ mergeConfig,
12
+ setStrictBlockConfig,
13
+ themes
14
+ } from "./chunk-IVINEQLU.js";
15
+
16
+ // src/cli.ts
17
+ import { writeFileSync, watch as fsWatch } from "fs";
18
+ import { basename, dirname, resolve } from "path";
19
+
20
+ // src/core/cli-helpers.ts
21
+ function formatModeTag(opts) {
22
+ const parts = [
23
+ opts.isStatic && "static",
24
+ opts.minify && "minified",
25
+ opts.cacheMode !== "normal" && `cache:${opts.cacheMode}`
26
+ ].filter(Boolean);
27
+ return parts.length ? ` (${parts.join(", ")})` : "";
28
+ }
29
+ function humanAge(seconds) {
30
+ if (!Number.isFinite(seconds) || seconds < 0) return "0s";
31
+ if (seconds < 60) return `${Math.floor(seconds)}s`;
32
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
33
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
34
+ return `${Math.floor(seconds / 86400)}d`;
35
+ }
36
+ function minifySvg(svg) {
37
+ return svg.replace(/>\s+</g, "><").replace(/\n\s+/g, "\n").replace(/^\s+|\s+$/g, "");
38
+ }
39
+ function resolveCacheMode(args2) {
40
+ const seen = [];
41
+ if (args2.includes("--no-cache")) seen.push({ flag: "--no-cache", mode: "off" });
42
+ if (args2.includes("--refresh-cache")) seen.push({ flag: "--refresh-cache", mode: "refresh" });
43
+ if (args2.includes("--frozen-cache")) seen.push({ flag: "--frozen-cache", mode: "frozen" });
44
+ const modeIdx = args2.indexOf("--cache-mode");
45
+ if (modeIdx !== -1) {
46
+ const value = args2[modeIdx + 1];
47
+ if (value === void 0 || value.startsWith("--")) {
48
+ throw new Error("--cache-mode requires a value (normal | refresh | frozen | off)");
49
+ }
50
+ if (!isCacheMode(value)) {
51
+ throw new Error(`Invalid --cache-mode value: ${value} (expected normal | refresh | frozen | off)`);
52
+ }
53
+ seen.push({ flag: `--cache-mode ${value}`, mode: value });
54
+ }
55
+ if (seen.length > 1) {
56
+ const flags = seen.map((s) => s.flag).join(" and ");
57
+ throw new Error(`Conflicting cache flags: can't combine ${flags}`);
58
+ }
59
+ return seen[0]?.mode ?? "normal";
60
+ }
61
+ function isCacheMode(s) {
62
+ return s === "normal" || s === "refresh" || s === "frozen" || s === "off";
63
+ }
64
+ var SECRET_KEY_RE = /token|secret|password|api[-_]?key|auth|credential|bearer|webhook[-_]?url/i;
65
+ function scrubSecrets(value, ancestors = /* @__PURE__ */ new Set()) {
66
+ if (Array.isArray(value)) {
67
+ if (ancestors.has(value)) return "[CIRCULAR]";
68
+ ancestors.add(value);
69
+ try {
70
+ return value.map((v) => scrubSecrets(v, ancestors));
71
+ } finally {
72
+ ancestors.delete(value);
73
+ }
74
+ }
75
+ if (value && typeof value === "object") {
76
+ if (ancestors.has(value)) return "[CIRCULAR]";
77
+ ancestors.add(value);
78
+ try {
79
+ const out = {};
80
+ for (const [k, v] of Object.entries(value)) {
81
+ out[k] = SECRET_KEY_RE.test(k) ? "[REDACTED]" : scrubSecrets(v, ancestors);
82
+ }
83
+ return out;
84
+ } finally {
85
+ ancestors.delete(value);
86
+ }
87
+ }
88
+ return value;
89
+ }
90
+ function formatZodType(t) {
91
+ if (!t || typeof t !== "object") return "unknown";
92
+ const ctor = t.constructor?.name ?? "unknown";
93
+ const def = t._def ?? {};
94
+ if (ctor === "ZodOptional" || ctor === "ZodNullable") {
95
+ return def.innerType ? formatZodType(def.innerType) : "unknown";
96
+ }
97
+ switch (ctor) {
98
+ case "ZodString":
99
+ return "string";
100
+ case "ZodNumber":
101
+ return "number";
102
+ case "ZodBoolean":
103
+ return "boolean";
104
+ case "ZodEnum": {
105
+ const opts = t.options ?? def.values ?? [];
106
+ return opts.map((v) => `"${v}"`).join(" | ") || "enum";
107
+ }
108
+ case "ZodArray": {
109
+ const elt = t.element ?? def.type;
110
+ return `array of ${formatZodType(elt)}`;
111
+ }
112
+ case "ZodRecord":
113
+ return `record<string, ${def.valueType ? formatZodType(def.valueType) : "unknown"}>`;
114
+ case "ZodObject":
115
+ return "object";
116
+ case "ZodLiteral":
117
+ return JSON.stringify(t.value);
118
+ case "ZodUnion":
119
+ return "union";
120
+ default:
121
+ return ctor.replace(/^Zod/, "").toLowerCase();
122
+ }
123
+ }
124
+ function isZodOptional(t) {
125
+ const ctor = t?.constructor?.name;
126
+ return ctor === "ZodOptional" || ctor === "ZodNullable";
127
+ }
128
+
129
+ // src/cli.ts
130
+ var VERSION = true ? "1.0.0" : "0.0.0-dev";
131
+ var args = process.argv.slice(2);
132
+ var command = args[0];
133
+ function getFlag(name) {
134
+ const idx = args.indexOf(`--${name}`);
135
+ if (idx === -1) return void 0;
136
+ return args[idx + 1];
137
+ }
138
+ function hasFlag(name) {
139
+ return args.includes(`--${name}`);
140
+ }
141
+ var GENERATE_KNOWN_FLAGS = /* @__PURE__ */ new Set([
142
+ "config",
143
+ "output",
144
+ "static",
145
+ "minify",
146
+ "strict",
147
+ "watch",
148
+ "no-cache",
149
+ "refresh-cache",
150
+ "frozen-cache",
151
+ "cache-mode",
152
+ "timings",
153
+ "explain"
154
+ ]);
155
+ function warnUnknownFlags(tokens, known) {
156
+ const valueFlags = /* @__PURE__ */ new Set(["config", "output", "cache-mode"]);
157
+ for (let i = 0; i < tokens.length; i++) {
158
+ const tok = tokens[i];
159
+ if (!tok.startsWith("--")) continue;
160
+ const name = tok.slice(2);
161
+ if (!known.has(name)) {
162
+ console.error(`\x1B[33m[svg-terminal] warning: unknown flag "${tok}" \u2014 ignoring\x1B[0m`);
163
+ }
164
+ if (valueFlags.has(name)) i++;
165
+ }
166
+ }
167
+ function formatWatchError(err) {
168
+ if (err instanceof ConfigError || err instanceof BlockConfigError) {
169
+ console.error(`\x1B[31m${err.formatted}\x1B[0m`);
170
+ } else {
171
+ console.error("\x1B[31mError:\x1B[0m", err instanceof Error ? err.message : err);
172
+ }
173
+ }
174
+ async function main() {
175
+ if (hasFlag("version") || command === "--version") {
176
+ console.log(`svg-terminal ${VERSION}`);
177
+ return;
178
+ }
179
+ switch (command) {
180
+ case "generate": {
181
+ warnUnknownFlags(args.slice(1), GENERATE_KNOWN_FLAGS);
182
+ const configPath = getFlag("config") ?? "terminal.yml";
183
+ const outputPath = getFlag("output") ?? "terminal.svg";
184
+ const isStatic = hasFlag("static");
185
+ const minify = hasFlag("minify");
186
+ const strict = hasFlag("strict");
187
+ const watch = hasFlag("watch");
188
+ const timings = hasFlag("timings");
189
+ const explain = hasFlag("explain");
190
+ const cacheMode = resolveCacheMode(args);
191
+ setStrictBlockConfig(strict);
192
+ const resolvedConfigPath = resolve(configPath);
193
+ const resolvedOutputPath = resolve(outputPath);
194
+ const modeTag = formatModeTag({ isStatic, minify, cacheMode });
195
+ const cacheStats = { hit: 0, miss: 0, refreshed: 0, fallback: 0, fallbacks: [] };
196
+ const onCacheEvent = (evt, key) => {
197
+ cacheStats[evt]++;
198
+ if (evt === "fallback") cacheStats.fallbacks.push(key);
199
+ };
200
+ const runOnce = async () => {
201
+ cacheStats.hit = 0;
202
+ cacheStats.miss = 0;
203
+ cacheStats.refreshed = 0;
204
+ cacheStats.fallback = 0;
205
+ cacheStats.fallbacks = [];
206
+ const start = performance.now();
207
+ const tLoadStart = performance.now();
208
+ const userConfig = await loadConfig(resolvedConfigPath);
209
+ const tLoadMs = performance.now() - tLoadStart;
210
+ if (explain) {
211
+ const merged = mergeConfig(userConfig);
212
+ const explainDump = scrubSecrets({
213
+ configPath: resolvedConfigPath,
214
+ outputPath: resolvedOutputPath,
215
+ theme: merged.theme.name,
216
+ window: merged.window,
217
+ text: merged.text,
218
+ effects: merged.effects,
219
+ variables: userConfig.variables,
220
+ blockCount: userConfig.blocks.length,
221
+ blocks: userConfig.blocks.map((entry) => {
222
+ const block = getBlock(entry.block);
223
+ return {
224
+ name: entry.block,
225
+ cacheable: block?.cacheable ?? false,
226
+ registered: !!block,
227
+ config: entry.config
228
+ };
229
+ }),
230
+ maxDuration: merged.maxDuration
231
+ });
232
+ console.error(`[svg-terminal --explain]
233
+ ${JSON.stringify(explainDump, null, 2)}`);
234
+ }
235
+ const genOpts = { configPath: resolvedConfigPath, cacheMode, onCacheEvent };
236
+ const tGenStart = performance.now();
237
+ let svg = isStatic ? await generateStatic(userConfig, genOpts) : await generate(userConfig, genOpts);
238
+ const tGenMs = performance.now() - tGenStart;
239
+ const tWriteStart = performance.now();
240
+ if (minify) svg = minifySvg(svg);
241
+ writeFileSync(resolvedOutputPath, svg, "utf-8");
242
+ const tWriteMs = performance.now() - tWriteStart;
243
+ const elapsed = Math.round(performance.now() - start);
244
+ const prefix = watch ? `[${(/* @__PURE__ */ new Date()).toTimeString().slice(0, 8)}] ` : "";
245
+ const duration = watch ? `, ${elapsed}ms` : "";
246
+ console.log(`${prefix}Generated ${outputPath}${modeTag} (${(svg.length / 1024).toFixed(1)} KB${duration})`);
247
+ if (timings) {
248
+ console.error(
249
+ `[svg-terminal --timings] load: ${tLoadMs.toFixed(1)}ms generate: ${tGenMs.toFixed(1)}ms write: ${tWriteMs.toFixed(1)}ms total: ${elapsed}ms`
250
+ );
251
+ }
252
+ const totalEvents = cacheStats.hit + cacheStats.miss + cacheStats.refreshed + cacheStats.fallback;
253
+ if (totalEvents > 0) {
254
+ const parts = [];
255
+ if (cacheStats.hit) parts.push(`\x1B[32mhit ${cacheStats.hit}\x1B[0m`);
256
+ if (cacheStats.miss) parts.push(`miss ${cacheStats.miss}`);
257
+ if (cacheStats.refreshed) parts.push(`refreshed ${cacheStats.refreshed}`);
258
+ if (cacheStats.fallback) parts.push(`\x1B[33mfallback ${cacheStats.fallback}\x1B[0m`);
259
+ const fallbackList = cacheStats.fallbacks.length > 0 ? ` [${cacheStats.fallbacks.join(", ")}]` : "";
260
+ console.error(`[svg-terminal cache] ${parts.join(" ")}${fallbackList}`);
261
+ }
262
+ };
263
+ if (!watch) {
264
+ await runOnce();
265
+ break;
266
+ }
267
+ try {
268
+ await runOnce();
269
+ } catch (e) {
270
+ formatWatchError(e);
271
+ }
272
+ console.log(`\x1B[2m[svg-terminal] watching ${configPath}... (Ctrl-C to exit)\x1B[0m`);
273
+ const DEBOUNCE_MS = 100;
274
+ let timer = null;
275
+ let running = false;
276
+ let queued = false;
277
+ const trigger = () => {
278
+ if (timer) clearTimeout(timer);
279
+ timer = setTimeout(async () => {
280
+ timer = null;
281
+ if (running) {
282
+ queued = true;
283
+ return;
284
+ }
285
+ running = true;
286
+ try {
287
+ await runOnce();
288
+ } catch (e) {
289
+ formatWatchError(e);
290
+ } finally {
291
+ running = false;
292
+ if (queued) {
293
+ queued = false;
294
+ trigger();
295
+ }
296
+ }
297
+ }, DEBOUNCE_MS);
298
+ };
299
+ const configDir = dirname(resolvedConfigPath);
300
+ const configName = basename(resolvedConfigPath);
301
+ const watcher = fsWatch(configDir, { persistent: true }, (_event, filename) => {
302
+ if (filename === configName) trigger();
303
+ });
304
+ watcher.on("error", (err) => {
305
+ console.error(`\x1B[31m[svg-terminal] watch error: ${err.message}\x1B[0m`);
306
+ });
307
+ const cleanup = () => {
308
+ watcher.close();
309
+ console.log("\n\x1B[2m[svg-terminal] stopped\x1B[0m");
310
+ process.exit(0);
311
+ };
312
+ process.on("SIGINT", cleanup);
313
+ process.on("SIGTERM", cleanup);
314
+ return;
315
+ }
316
+ case "init": {
317
+ const starter = `# svg-terminal configuration
318
+ # See: https://github.com/williamzujkowski/svg-terminal
319
+
320
+ theme: dracula
321
+
322
+ window:
323
+ width: 1000
324
+ height: 560
325
+ title: "user@terminal:~"
326
+ # style: macos # macos | win95 | floating | minimal | none
327
+ # autoHeight: false # Auto-calculate height from content
328
+ # minHeight: 300 # Minimum height when autoHeight is true
329
+ # maxHeight: 1200 # Maximum height when autoHeight is true
330
+
331
+ terminal:
332
+ prompt: "user@host:~$ "
333
+ fontSize: 14
334
+
335
+ effects:
336
+ textGlow: false # phosphor halo \u2014 try true with amber / green-phosphor / cyberpunk
337
+ shadow: true
338
+ scanlines: true
339
+
340
+ # Animation timing (all values in ms)
341
+ # animation:
342
+ # cursorBlinkCycle: 1000
343
+ # outputLineStagger: 50
344
+ # commandOutputPause: 300
345
+ # loop: true # true (infinite) | false (play once) | number (N times)
346
+
347
+ # Window chrome appearance
348
+ # chrome:
349
+ # titleFontSize: 13
350
+ # dimOpacity: 0.6
351
+
352
+ blocks:
353
+ - block: neofetch
354
+ config:
355
+ username: user
356
+ hostname: terminal
357
+ os: TerminalOS v1.0
358
+ shell: bash 5.2
359
+ role: Developer
360
+ languages: TypeScript, Python
361
+
362
+ - block: fortune
363
+ config:
364
+ fortunes:
365
+ - "The best code is no code at all."
366
+ - "Talk is cheap. Show me the code."
367
+ - "First, solve the problem. Then, write the code."
368
+
369
+ # Uncomment to see an animated block (the library's signature feature) \u2014
370
+ # spinners, clocks, dice rolls. Multi-frame animation, single-line restriction.
371
+ # - block: loading-spinner
372
+ # config:
373
+ # label: "deploying to production"
374
+
375
+ - block: custom
376
+ config:
377
+ command: echo "Thanks for visiting!"
378
+ lines:
379
+ - "[[fg:green]]Thanks for visiting my profile![[/fg]]"
380
+ - ""
381
+ - "Have a great day!"
382
+ `;
383
+ const targetPath = resolve("terminal.yml");
384
+ try {
385
+ writeFileSync(targetPath, starter, { encoding: "utf-8", flag: hasFlag("force") ? "w" : "wx" });
386
+ } catch (err) {
387
+ if (err.code === "EEXIST") {
388
+ console.error("terminal.yml already exists. Use --force to overwrite.");
389
+ process.exit(1);
390
+ }
391
+ throw err;
392
+ }
393
+ console.log("Created terminal.yml \u2014 edit it and run: svg-terminal generate");
394
+ break;
395
+ }
396
+ case "themes": {
397
+ console.log("Available themes:");
398
+ for (const name of Object.keys(themes)) {
399
+ console.log(` - ${name}`);
400
+ }
401
+ break;
402
+ }
403
+ case "blocks": {
404
+ const target = args[1];
405
+ if (target) {
406
+ const block = getBlock(target);
407
+ if (!block) {
408
+ console.error(`Unknown block "${target}". Run: svg-terminal blocks`);
409
+ process.exit(1);
410
+ }
411
+ console.log(`${block.name} \u2014 ${block.description}`);
412
+ if (block.cacheable) console.log(" cacheable: yes (participates in .svg-terminal-cache.json)");
413
+ const schema = block.configSchema;
414
+ const shape = schema?.shape;
415
+ if (!shape || Object.keys(shape).length === 0) {
416
+ console.log(" Config: (no fields)");
417
+ break;
418
+ }
419
+ const names = Object.keys(shape);
420
+ const w = Math.max(...names.map((n) => n.length));
421
+ console.log(" Config:");
422
+ for (const name of names) {
423
+ const t = shape[name];
424
+ const tag = isZodOptional(t) ? "" : " (required)";
425
+ console.log(` ${name.padEnd(w)} ${formatZodType(t)}${tag}`);
426
+ }
427
+ console.log("");
428
+ console.log(` Plus the universal entry-level keys: command, color, typing, pause`);
429
+ break;
430
+ }
431
+ console.log("Available blocks (cacheable blocks marked *):");
432
+ for (const name of listBlocks()) {
433
+ const block = getBlock(name);
434
+ const mark = block?.cacheable ? " *" : "";
435
+ console.log(` - ${name}${mark}`);
436
+ }
437
+ console.log("");
438
+ console.log("Run `svg-terminal blocks <name>` to see a block's config schema.");
439
+ break;
440
+ }
441
+ case "cache": {
442
+ const sub = args[1];
443
+ if (sub !== "check") {
444
+ console.error("Usage: svg-terminal cache check [--config <path>]");
445
+ process.exit(1);
446
+ }
447
+ const configPath = getFlag("config") ?? "terminal.yml";
448
+ const resolved = resolve(configPath);
449
+ const userConfig = await loadConfig(resolved);
450
+ const { filePath, results } = inspectCache(userConfig, resolved);
451
+ const w = Math.max(0, ...results.map((r) => r.key.length));
452
+ console.log(`Checking cache at ${filePath}...`);
453
+ if (results.length === 0) {
454
+ console.log(" (no cacheable blocks in this config)");
455
+ break;
456
+ }
457
+ let bad = 0;
458
+ for (const r of results) {
459
+ const ageStr = r.ageSeconds !== void 0 ? `age ${humanAge(r.ageSeconds)}` : "";
460
+ const tag = r.status === "OK" ? `\x1B[32mOK\x1B[0m ` : r.status === "STALE" ? `\x1B[33mSTALE\x1B[0m` : `\x1B[31mMISS\x1B[0m `;
461
+ console.log(` ${r.key.padEnd(w)} ${tag} ${ageStr}`);
462
+ if (r.status !== "OK") bad++;
463
+ }
464
+ if (bad > 0) {
465
+ console.error(`
466
+ ${bad} cacheable block(s) need a refresh. Run: svg-terminal generate --refresh-cache`);
467
+ process.exit(1);
468
+ }
469
+ break;
470
+ }
471
+ default: {
472
+ console.log(`svg-terminal \u2014 Generate animated SVG terminals
473
+
474
+ Commands:
475
+ generate Generate SVG from config file
476
+ init Create a starter terminal.yml (refuses to overwrite without --force)
477
+ themes List available themes
478
+ blocks [<name>] List available block types, or print one block's config schema
479
+ cache check Verify dynamic-block cache freshness (exit 1 on stale/missing)
480
+
481
+ Generate options:
482
+ --config <path> Config file path (default: terminal.yml)
483
+ --output <path> Output file path (default: terminal.svg)
484
+ --static Render the non-animated final-frame snapshot
485
+ --minify Strip inter-element whitespace for smaller output
486
+ --strict Promote unknown-block-config-key warnings to hard errors
487
+ --watch Re-generate on config file change (Ctrl-C to exit)
488
+ --timings Print per-phase wall-clock timings (load, generate, write) to stderr
489
+ --explain Print the resolved config + block list as JSON to stderr
490
+
491
+ Cache events appear in stderr automatically when any dynamic block
492
+ participates ([svg-terminal cache] hit N miss N fallback N [block-names]).
493
+
494
+ Cache modes (mutually exclusive \u2014 defaults to normal):
495
+ --no-cache Don't read or write the dynamic-block fetch cache
496
+ --refresh-cache Force refresh: ignore existing cache entries, re-fetch all
497
+ --frozen-cache Use cached values only; never fetch (CI offline mode)
498
+ --cache-mode <m> Explicit: normal | refresh | frozen | off
499
+
500
+ Init options:
501
+ --force Overwrite an existing terminal.yml
502
+
503
+ Global:
504
+ --version Print version number
505
+
506
+ Examples:
507
+ svg-terminal init
508
+ svg-terminal generate --config terminal.yml --output terminal.svg
509
+ svg-terminal generate --static
510
+ svg-terminal generate --watch
511
+ svg-terminal cache check --config terminal.yml
512
+ `);
513
+ }
514
+ }
515
+ }
516
+ main().catch((err) => {
517
+ if (err instanceof ConfigError || err instanceof BlockConfigError) {
518
+ console.error(err.formatted);
519
+ } else {
520
+ console.error("Error:", err instanceof Error ? err.message : err);
521
+ }
522
+ process.exit(1);
523
+ });
524
+ //# sourceMappingURL=cli.js.map