portable-agent-layer 0.28.1 → 0.29.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/assets/skills/presentation/README.md +21 -0
- package/assets/skills/presentation/SKILL.md +172 -0
- package/assets/skills/presentation/demo/slides/001-title.md +5 -0
- package/assets/skills/presentation/demo/slides/002-agenda.md +13 -0
- package/assets/skills/presentation/demo/slides/003-section-text-heavy.md +3 -0
- package/assets/skills/presentation/demo/slides/004-content.md +12 -0
- package/assets/skills/presentation/demo/slides/005-two-column.md +22 -0
- package/assets/skills/presentation/demo/slides/006-quote.md +4 -0
- package/assets/skills/presentation/demo/slides/007-section-structured.md +3 -0
- package/assets/skills/presentation/demo/slides/008-table.md +11 -0
- package/assets/skills/presentation/demo/slides/009-comparison.md +25 -0
- package/assets/skills/presentation/demo/slides/010-image-text.md +20 -0
- package/assets/skills/presentation/demo/slides/011-code.md +19 -0
- package/assets/skills/presentation/demo/slides/012-closing.md +5 -0
- package/assets/skills/presentation/template/README.md +15 -0
- package/assets/skills/presentation/template/slides/001-title.md +5 -0
- package/assets/skills/presentation/template/slides/002-content.md +6 -0
- package/assets/skills/presentation/template/slides/003-closing.md +3 -0
- package/assets/skills/presentation/theme-base/base.css +167 -0
- package/assets/skills/presentation/theme-base/layouts.css +216 -0
- package/assets/skills/presentation/theme-base/skeleton.html +53 -0
- package/assets/skills/presentation/tools/build.ts +160 -0
- package/assets/skills/presentation/tools/lib/inline.ts +35 -0
- package/assets/skills/presentation/tools/lib/paths.ts +16 -0
- package/assets/skills/presentation/tools/lib/registry.ts +59 -0
- package/assets/skills/presentation/tools/list-templates.ts +21 -0
- package/assets/skills/presentation/tools/new-deck.ts +101 -0
- package/assets/skills/presentation/tools/present.ts +70 -0
- package/assets/skills/presentation/tools/setup-template.ts +351 -0
- package/assets/skills/presentation/vendor/reveal/plugin/highlight/highlight.js +5 -0
- package/assets/skills/presentation/vendor/reveal/plugin/highlight/monokai.css +71 -0
- package/assets/skills/presentation/vendor/reveal/plugin/markdown/markdown.js +7 -0
- package/assets/skills/presentation/vendor/reveal/plugin/notes/notes.js +1 -0
- package/assets/skills/presentation/vendor/reveal/reveal.css +8 -0
- package/assets/skills/presentation/vendor/reveal/reveal.js +9 -0
- package/assets/templates/settings.claude.json +20 -0
- package/package.json +1 -1
- package/src/hooks/CompactRecover.ts +105 -0
- package/src/hooks/PreCompactPersist.ts +86 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// presentation skill — build (if stale) and open the deck in default browser.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// bun present.ts <deck-dir>
|
|
7
|
+
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { constants as fsConst } from "node:fs";
|
|
10
|
+
import { access, stat } from "node:fs/promises";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
async function exists(p: string): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
await access(p, fsConst.F_OK);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function isStale(deckDir: string, distHtml: string): Promise<boolean> {
|
|
24
|
+
if (!(await exists(distHtml))) return true;
|
|
25
|
+
const distMtime = (await stat(distHtml)).mtimeMs;
|
|
26
|
+
const candidates = ["content.md", "slides.config.yml", "overrides.css"];
|
|
27
|
+
for (const f of candidates) {
|
|
28
|
+
const p = join(deckDir, f);
|
|
29
|
+
if (await exists(p)) {
|
|
30
|
+
if ((await stat(p)).mtimeMs > distMtime) return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main() {
|
|
37
|
+
const argv = process.argv.slice(2);
|
|
38
|
+
if (argv.length === 0) {
|
|
39
|
+
console.error("usage: present.ts <deck-dir>");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const deckDir = resolve(argv[0]);
|
|
43
|
+
const distHtml = join(deckDir, "dist", "index.html");
|
|
44
|
+
|
|
45
|
+
if (await isStale(deckDir, distHtml)) {
|
|
46
|
+
console.log("→ rebuilding (deck has changed since last build)…");
|
|
47
|
+
const buildScript = fileURLToPath(new URL("./build.ts", import.meta.url));
|
|
48
|
+
await new Promise<void>((res, rej) => {
|
|
49
|
+
const p = spawn("bun", [buildScript, deckDir], { stdio: "inherit" });
|
|
50
|
+
p.on("exit", (c) => (c === 0 ? res() : rej(new Error(`build exited ${c}`))));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const opener =
|
|
55
|
+
process.platform === "darwin"
|
|
56
|
+
? "open"
|
|
57
|
+
: process.platform === "win32"
|
|
58
|
+
? "start"
|
|
59
|
+
: "xdg-open";
|
|
60
|
+
spawn(opener, [distHtml], { detached: true, stdio: "ignore" }).unref();
|
|
61
|
+
console.log(`→ opened ${distHtml}`);
|
|
62
|
+
console.log(
|
|
63
|
+
" F = fullscreen · S = speaker notes · ? = keyboard shortcuts · Esc = overview"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main().catch((e) => {
|
|
68
|
+
console.error(e?.message ?? e);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// presentation skill — set up a brand template.
|
|
4
|
+
// Interactive Q&A by default; flag-driven for non-interactive use (Claude / scripts).
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// bun setup-template.ts # interactive
|
|
8
|
+
// bun setup-template.ts --name <slug> --logo <path> --primary "#0E1335" [--accent "#..."] \
|
|
9
|
+
// [--footer "..."] [--logo-placement footer] [--fonts system] [--aspect 16:9] \
|
|
10
|
+
// [--showcase] [--yes]
|
|
11
|
+
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { constants as fsConst } from "node:fs";
|
|
14
|
+
import { access, copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
15
|
+
import { extname, join, resolve } from "node:path";
|
|
16
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
17
|
+
import * as readline from "node:readline/promises";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
import { TEMPLATES_ROOT } from "./lib/paths";
|
|
21
|
+
import { type Aspect, type LogoPlacement, registerTemplate } from "./lib/registry";
|
|
22
|
+
|
|
23
|
+
type Args = Partial<{
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
logo: string;
|
|
27
|
+
primary: string;
|
|
28
|
+
accent: string;
|
|
29
|
+
footer: string;
|
|
30
|
+
logoPlacement: LogoPlacement;
|
|
31
|
+
fonts: string;
|
|
32
|
+
aspect: Aspect;
|
|
33
|
+
showcase: boolean;
|
|
34
|
+
yes: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv: string[]): Args {
|
|
38
|
+
const a: Args = {};
|
|
39
|
+
for (let i = 0; i < argv.length; i++) {
|
|
40
|
+
const cur = argv[i];
|
|
41
|
+
const next = argv[i + 1];
|
|
42
|
+
switch (cur) {
|
|
43
|
+
case "--name":
|
|
44
|
+
a.name = next;
|
|
45
|
+
i++;
|
|
46
|
+
break;
|
|
47
|
+
case "--path":
|
|
48
|
+
a.path = next;
|
|
49
|
+
i++;
|
|
50
|
+
break;
|
|
51
|
+
case "--logo":
|
|
52
|
+
a.logo = next;
|
|
53
|
+
i++;
|
|
54
|
+
break;
|
|
55
|
+
case "--primary":
|
|
56
|
+
a.primary = next;
|
|
57
|
+
i++;
|
|
58
|
+
break;
|
|
59
|
+
case "--accent":
|
|
60
|
+
a.accent = next;
|
|
61
|
+
i++;
|
|
62
|
+
break;
|
|
63
|
+
case "--footer":
|
|
64
|
+
a.footer = next;
|
|
65
|
+
i++;
|
|
66
|
+
break;
|
|
67
|
+
case "--logo-placement":
|
|
68
|
+
a.logoPlacement = next as LogoPlacement;
|
|
69
|
+
i++;
|
|
70
|
+
break;
|
|
71
|
+
case "--fonts":
|
|
72
|
+
a.fonts = next;
|
|
73
|
+
i++;
|
|
74
|
+
break;
|
|
75
|
+
case "--aspect":
|
|
76
|
+
a.aspect = next as Aspect;
|
|
77
|
+
i++;
|
|
78
|
+
break;
|
|
79
|
+
case "--showcase":
|
|
80
|
+
a.showcase = true;
|
|
81
|
+
break;
|
|
82
|
+
case "--yes":
|
|
83
|
+
case "-y":
|
|
84
|
+
a.yes = true;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return a;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function exists(p: string): Promise<boolean> {
|
|
92
|
+
try {
|
|
93
|
+
await access(p, fsConst.F_OK);
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const isHex = (s: string) => /^#[0-9a-fA-F]{6}$/.test(s);
|
|
101
|
+
|
|
102
|
+
// HSL hue rotation by 180° → suggested complementary; brighten lightness for dark primaries
|
|
103
|
+
// so a deep navy gives a warm/light accent rather than another dark colour.
|
|
104
|
+
function complementary(hex: string): string {
|
|
105
|
+
const h = hex.replace("#", "");
|
|
106
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
107
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
108
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
109
|
+
const max = Math.max(r, g, b),
|
|
110
|
+
min = Math.min(r, g, b);
|
|
111
|
+
let H = 0,
|
|
112
|
+
S = 0;
|
|
113
|
+
const L = (max + min) / 2;
|
|
114
|
+
if (max !== min) {
|
|
115
|
+
const d = max - min;
|
|
116
|
+
S = L > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
117
|
+
if (max === r) H = (g - b) / d + (g < b ? 6 : 0);
|
|
118
|
+
else if (max === g) H = (b - r) / d + 2;
|
|
119
|
+
else H = (r - g) / d + 4;
|
|
120
|
+
H *= 60;
|
|
121
|
+
}
|
|
122
|
+
H = (H + 180) % 360;
|
|
123
|
+
const newL = Math.min(0.78, Math.max(0.5, L + 0.35));
|
|
124
|
+
const newS = Math.max(S, 0.55);
|
|
125
|
+
const c = (1 - Math.abs(2 * newL - 1)) * newS;
|
|
126
|
+
const x = c * (1 - Math.abs(((H / 60) % 2) - 1));
|
|
127
|
+
const m = newL - c / 2;
|
|
128
|
+
let [rr, gg, bb] = [0, 0, 0];
|
|
129
|
+
if (H < 60) [rr, gg, bb] = [c, x, 0];
|
|
130
|
+
else if (H < 120) [rr, gg, bb] = [x, c, 0];
|
|
131
|
+
else if (H < 180) [rr, gg, bb] = [0, c, x];
|
|
132
|
+
else if (H < 240) [rr, gg, bb] = [0, x, c];
|
|
133
|
+
else if (H < 300) [rr, gg, bb] = [x, 0, c];
|
|
134
|
+
else [rr, gg, bb] = [c, 0, x];
|
|
135
|
+
const toHex = (n: number) =>
|
|
136
|
+
Math.round((n + m) * 255)
|
|
137
|
+
.toString(16)
|
|
138
|
+
.padStart(2, "0")
|
|
139
|
+
.toUpperCase();
|
|
140
|
+
return `#${toHex(rr)}${toHex(gg)}${toHex(bb)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function ask(rl: readline.Interface, q: string, def?: string): Promise<string> {
|
|
144
|
+
const p = def ? `${q} [${def}] ` : `${q} `;
|
|
145
|
+
const ans = (await rl.question(p)).trim();
|
|
146
|
+
return ans || def || "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function main() {
|
|
150
|
+
const args = parseArgs(process.argv.slice(2));
|
|
151
|
+
const interactive = !(args.name && args.logo && args.primary);
|
|
152
|
+
const rl = interactive ? readline.createInterface({ input, output }) : null;
|
|
153
|
+
|
|
154
|
+
// 1. Name
|
|
155
|
+
let name = args.name;
|
|
156
|
+
if (!name && rl) {
|
|
157
|
+
while (true) {
|
|
158
|
+
name = await ask(rl, "Template name (slug, e.g. 'my-company'):");
|
|
159
|
+
if (/^[a-z0-9][a-z0-9-]*$/.test(name)) break;
|
|
160
|
+
console.error(" ✘ name must be lowercase alphanumerics + dashes");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!name) {
|
|
164
|
+
console.error("--name required");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. Path
|
|
169
|
+
const defaultPath = join(TEMPLATES_ROOT, name);
|
|
170
|
+
const tplPath = args.path
|
|
171
|
+
? resolve(args.path)
|
|
172
|
+
: rl
|
|
173
|
+
? resolve(await ask(rl, "Storage path:", defaultPath))
|
|
174
|
+
: defaultPath;
|
|
175
|
+
|
|
176
|
+
// 3. Logo
|
|
177
|
+
let logo = args.logo;
|
|
178
|
+
if (!logo && rl) {
|
|
179
|
+
while (true) {
|
|
180
|
+
logo = (await rl.question("Logo file path (.svg preferred, .png ok): ")).trim();
|
|
181
|
+
if (logo && (await exists(resolve(logo)))) break;
|
|
182
|
+
console.error(` ✘ file not found: ${logo}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!logo) {
|
|
186
|
+
console.error("--logo required");
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
logo = resolve(logo);
|
|
190
|
+
if (!(await exists(logo))) {
|
|
191
|
+
console.error(`logo not found: ${logo}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 4. Primary
|
|
196
|
+
let primary = args.primary;
|
|
197
|
+
if (!primary && rl) {
|
|
198
|
+
while (true) {
|
|
199
|
+
primary = (await rl.question("Primary brand color (hex, e.g. #0E1335): ")).trim();
|
|
200
|
+
if (isHex(primary)) break;
|
|
201
|
+
console.error(" ✘ must be 6-digit hex like #0E1335");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!primary || !isHex(primary)) {
|
|
205
|
+
console.error("--primary must be hex like #0E1335");
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 5. Accent — default = derived complementary
|
|
210
|
+
const suggested = complementary(primary);
|
|
211
|
+
let accent = args.accent;
|
|
212
|
+
if (!accent && rl) {
|
|
213
|
+
accent = await ask(rl, `Accent color (hex) [derived: ${suggested}]:`, suggested);
|
|
214
|
+
if (!isHex(accent)) accent = suggested;
|
|
215
|
+
}
|
|
216
|
+
if (!accent || !isHex(accent)) accent = suggested;
|
|
217
|
+
|
|
218
|
+
// 6. Footer
|
|
219
|
+
const footer =
|
|
220
|
+
args.footer ?? (rl ? await ask(rl, "Footer text (blank for none):", "") : "");
|
|
221
|
+
|
|
222
|
+
// 7. Logo placement
|
|
223
|
+
let logoPlacement = args.logoPlacement;
|
|
224
|
+
if (!logoPlacement && rl) {
|
|
225
|
+
const a = await ask(
|
|
226
|
+
rl,
|
|
227
|
+
"Logo placement [cover-only / footer / both / none]:",
|
|
228
|
+
"footer"
|
|
229
|
+
);
|
|
230
|
+
logoPlacement = (
|
|
231
|
+
["cover-only", "footer", "both", "none"].includes(a) ? a : "footer"
|
|
232
|
+
) as LogoPlacement;
|
|
233
|
+
}
|
|
234
|
+
if (!logoPlacement) logoPlacement = "footer";
|
|
235
|
+
|
|
236
|
+
// 8. Fonts
|
|
237
|
+
const fonts =
|
|
238
|
+
args.fonts ??
|
|
239
|
+
(rl ? await ask(rl, "Fonts ['system' or Google Fonts URL]:", "system") : "system");
|
|
240
|
+
|
|
241
|
+
// 9. Aspect
|
|
242
|
+
let aspect = args.aspect;
|
|
243
|
+
if (!aspect && rl) {
|
|
244
|
+
const a = await ask(rl, "Aspect ratio [16:9 / 4:3 / 16:10]:", "16:9");
|
|
245
|
+
aspect = (["16:9", "4:3", "16:10"].includes(a) ? a : "16:9") as Aspect;
|
|
246
|
+
}
|
|
247
|
+
if (!aspect) aspect = "16:9";
|
|
248
|
+
|
|
249
|
+
// 10. Showcase deck?
|
|
250
|
+
let showcase = args.showcase;
|
|
251
|
+
if (showcase === undefined && rl) {
|
|
252
|
+
const a = (
|
|
253
|
+
await ask(rl, "Generate showcase deck demonstrating every layout? (y/n):", "n")
|
|
254
|
+
).toLowerCase();
|
|
255
|
+
showcase = a.startsWith("y");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (rl) {
|
|
259
|
+
console.log("\nSummary:");
|
|
260
|
+
console.log(` name ${name}`);
|
|
261
|
+
console.log(` path ${tplPath}`);
|
|
262
|
+
console.log(` logo ${logo}`);
|
|
263
|
+
console.log(` primary ${primary}`);
|
|
264
|
+
console.log(` accent ${accent}`);
|
|
265
|
+
console.log(` footer ${footer || "(none)"}`);
|
|
266
|
+
console.log(` logoPlacement ${logoPlacement}`);
|
|
267
|
+
console.log(` fonts ${fonts}`);
|
|
268
|
+
console.log(` aspect ${aspect}`);
|
|
269
|
+
console.log(` showcase ${showcase ? "yes" : "no"}`);
|
|
270
|
+
if (!args.yes) {
|
|
271
|
+
const ok = (await rl.question("\nWrite template? (y/n) [y]: "))
|
|
272
|
+
.trim()
|
|
273
|
+
.toLowerCase();
|
|
274
|
+
if (ok && !ok.startsWith("y")) {
|
|
275
|
+
console.log("aborted");
|
|
276
|
+
rl.close();
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
rl.close();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Write template files
|
|
284
|
+
await mkdir(tplPath, { recursive: true });
|
|
285
|
+
const ext = extname(logo).toLowerCase() || ".svg";
|
|
286
|
+
await copyFile(logo, join(tplPath, `logo${ext}`));
|
|
287
|
+
|
|
288
|
+
// template.css
|
|
289
|
+
const css = `/* template: ${name} — generated by setup-template.ts */
|
|
290
|
+
:root {
|
|
291
|
+
--brand-primary: ${primary};
|
|
292
|
+
--brand-accent: ${accent};
|
|
293
|
+
}
|
|
294
|
+
`;
|
|
295
|
+
await writeFile(join(tplPath, "template.css"), css, "utf8");
|
|
296
|
+
|
|
297
|
+
// template.yml
|
|
298
|
+
const yml = `# template: ${name}
|
|
299
|
+
primary: "${primary}"
|
|
300
|
+
accent: "${accent}"
|
|
301
|
+
footer: ${JSON.stringify(footer)}
|
|
302
|
+
logoPlacement: ${logoPlacement}
|
|
303
|
+
logo: "logo${ext}"
|
|
304
|
+
fonts: ${JSON.stringify(fonts)}
|
|
305
|
+
aspect: "${aspect}"
|
|
306
|
+
`;
|
|
307
|
+
await writeFile(join(tplPath, "template.yml"), yml, "utf8");
|
|
308
|
+
|
|
309
|
+
// Register
|
|
310
|
+
await registerTemplate({
|
|
311
|
+
name,
|
|
312
|
+
path: tplPath,
|
|
313
|
+
meta: { primary, accent, footer, logoPlacement, fonts, aspect },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
console.log(`\n✓ template "${name}" registered`);
|
|
317
|
+
console.log(` path: ${tplPath}`);
|
|
318
|
+
|
|
319
|
+
if (showcase) {
|
|
320
|
+
const showcaseDir = join(tplPath, "showcase-deck");
|
|
321
|
+
const newDeckScript = fileURLToPath(new URL("./new-deck.ts", import.meta.url));
|
|
322
|
+
await new Promise<void>((res, rej) => {
|
|
323
|
+
const p = spawn(
|
|
324
|
+
"bun",
|
|
325
|
+
[
|
|
326
|
+
newDeckScript,
|
|
327
|
+
showcaseDir,
|
|
328
|
+
"--template",
|
|
329
|
+
name,
|
|
330
|
+
"--showcase",
|
|
331
|
+
"--title",
|
|
332
|
+
`${name} — showcase`,
|
|
333
|
+
],
|
|
334
|
+
{ stdio: "inherit" }
|
|
335
|
+
);
|
|
336
|
+
p.on("exit", (code) =>
|
|
337
|
+
code === 0 ? res() : rej(new Error(`new-deck exited ${code}`))
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
console.log(`\n showcase deck: ${showcaseDir}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log(
|
|
344
|
+
`\nNext: bun ~/.pal/skills/presentation/tools/new-deck.ts <deck-dir> --template ${name}`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
main().catch((e) => {
|
|
349
|
+
console.error(e?.message ?? e);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
});
|