portable-agent-layer 0.29.1 → 0.30.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/consulting-report/tools/generate-pdf.ts +4 -5
- package/assets/skills/create-skill/SKILL.md +55 -18
- package/assets/skills/presentation/README.md +11 -5
- package/assets/skills/presentation/SKILL.md +93 -17
- package/assets/skills/presentation/demo/slides/011a-big-stat.md +6 -0
- package/assets/skills/presentation/demo/slides/011b-metric-grid.md +22 -0
- package/assets/skills/presentation/demo/slides/011c-pull-quote.md +6 -0
- package/assets/skills/presentation/demo/slides/012-closing.md +2 -2
- package/assets/skills/presentation/template/README.md +4 -3
- package/assets/skills/presentation/theme-base/base.css +193 -35
- package/assets/skills/presentation/theme-base/layouts.css +224 -42
- package/assets/skills/presentation/tools/build.ts +70 -26
- package/assets/skills/presentation/tools/doctor.ts +406 -0
- package/assets/skills/presentation/tools/new-deck.ts +9 -2
- package/package.json +3 -1
- package/src/targets/lib.ts +1 -4
- package/assets/skills/presentation/tools/present.ts +0 -70
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// presentation skill — lint a deck before build.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// bun doctor.ts <deck-dir> [--strict]
|
|
6
|
+
//
|
|
7
|
+
// Reads slides/*.md (or legacy content.md), runs layout-aware lint rules,
|
|
8
|
+
// prints per-slide findings + a summary, and exits 0 (clean) or 1 (errors).
|
|
9
|
+
// --strict promotes warnings to errors.
|
|
10
|
+
//
|
|
11
|
+
// Rules are heuristic — thresholds documented in SKILL.md. Doctor is a
|
|
12
|
+
// safety-net, not a style guide; intentionally permissive.
|
|
13
|
+
|
|
14
|
+
import { constants as fsConst } from "node:fs";
|
|
15
|
+
import { access, readdir } from "node:fs/promises";
|
|
16
|
+
import { basename, join, resolve } from "node:path";
|
|
17
|
+
import { readText } from "./lib/inline";
|
|
18
|
+
|
|
19
|
+
export type Severity = "E" | "W";
|
|
20
|
+
export type Finding = { rule: string; severity: Severity; msg: string };
|
|
21
|
+
export type SlideReport = { name: string; layout: string; findings: Finding[] };
|
|
22
|
+
|
|
23
|
+
async function exists(p: string): Promise<boolean> {
|
|
24
|
+
try {
|
|
25
|
+
await access(p, fsConst.F_OK);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadSlides(deckDir: string): Promise<{ name: string; body: string }[]> {
|
|
33
|
+
const slidesDir = join(deckDir, "slides");
|
|
34
|
+
if (await exists(slidesDir)) {
|
|
35
|
+
const files = (await readdir(slidesDir)).filter((f) => f.endsWith(".md")).sort();
|
|
36
|
+
if (files.length === 0) throw new Error(`slides/ is empty at ${slidesDir}`);
|
|
37
|
+
return Promise.all(
|
|
38
|
+
files.map(async (f) => ({ name: f, body: await readText(join(slidesDir, f)) }))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const legacy = join(deckDir, "content.md");
|
|
42
|
+
if (await exists(legacy)) {
|
|
43
|
+
const raw = await readText(legacy);
|
|
44
|
+
return raw.split(/^---$/m).map((body, i) => ({ name: `slide-${i + 1}`, body }));
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`no slides/ directory or content.md found in ${deckDir}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function extractLayout(body: string): string {
|
|
50
|
+
const m = /<!--\s*\.slide:\s*data-layout="([^"]+)"\s*-->/i.exec(body);
|
|
51
|
+
return m ? m[1] : "content";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stripNotes(body: string): string {
|
|
55
|
+
// Remove speaker notes — every line from `Note:` onward at line start.
|
|
56
|
+
const lines = body.split("\n");
|
|
57
|
+
const cut = lines.findIndex((l) => /^Note:/i.test(l.trim()));
|
|
58
|
+
return cut === -1 ? body : lines.slice(0, cut).join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function countAtxHeading(body: string, level: 1 | 2): string[] {
|
|
62
|
+
const re = new RegExp(`^#{${level}}\\s+(.+?)\\s*$`, "gm");
|
|
63
|
+
return Array.from(body.matchAll(re), (m) => m[1]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function countTopLevelListItems(body: string): number {
|
|
67
|
+
// Count lines starting with `- `, `* `, or `N. ` at column 0 (no leading indent).
|
|
68
|
+
let n = 0;
|
|
69
|
+
for (const line of body.split("\n")) {
|
|
70
|
+
if (/^(?:[-*]\s+|\d+\.\s+)/.test(line)) n++;
|
|
71
|
+
}
|
|
72
|
+
return n;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findImageRefs(body: string): string[] {
|
|
76
|
+
// Skip lines inside fenced code blocks — they're examples, not references.
|
|
77
|
+
const out: string[] = [];
|
|
78
|
+
const lines = body.split("\n");
|
|
79
|
+
let inFence = false;
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
if (/^```/.test(line)) {
|
|
82
|
+
inFence = !inFence;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (inFence) continue;
|
|
86
|
+
for (const m of line.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
87
|
+
const ref = m[1].trim();
|
|
88
|
+
if (!/^(https?:|data:)/i.test(ref)) out.push(ref);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function codeBlockLineCounts(body: string): number[] {
|
|
95
|
+
const counts: number[] = [];
|
|
96
|
+
const lines = body.split("\n");
|
|
97
|
+
let inBlock = false;
|
|
98
|
+
let n = 0;
|
|
99
|
+
for (const l of lines) {
|
|
100
|
+
if (/^```/.test(l)) {
|
|
101
|
+
if (inBlock) {
|
|
102
|
+
counts.push(n);
|
|
103
|
+
n = 0;
|
|
104
|
+
inBlock = false;
|
|
105
|
+
} else {
|
|
106
|
+
inBlock = true;
|
|
107
|
+
}
|
|
108
|
+
} else if (inBlock) {
|
|
109
|
+
n++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return counts;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function lintSlide(
|
|
116
|
+
slide: { name: string; body: string },
|
|
117
|
+
deckDir: string
|
|
118
|
+
): Promise<SlideReport> {
|
|
119
|
+
const layout = extractLayout(slide.body);
|
|
120
|
+
const body = stripNotes(slide.body);
|
|
121
|
+
const findings: Finding[] = [];
|
|
122
|
+
|
|
123
|
+
const has = (s: string) => body.includes(s);
|
|
124
|
+
const heads1 = countAtxHeading(body, 1);
|
|
125
|
+
const heads2 = countAtxHeading(body, 2);
|
|
126
|
+
|
|
127
|
+
// ── Global rules ──────────────────────────────────────────────────────
|
|
128
|
+
if (!/<!--\s*\.slide:\s*data-layout=/i.test(slide.body)) {
|
|
129
|
+
findings.push({
|
|
130
|
+
rule: "no-layout",
|
|
131
|
+
severity: "W",
|
|
132
|
+
msg: "no <!-- .slide: data-layout=\"...\" --> directive — defaults to 'content'",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
for (const h of heads1) {
|
|
136
|
+
if (h.length > 60) {
|
|
137
|
+
findings.push({
|
|
138
|
+
rule: "long-title",
|
|
139
|
+
severity: "W",
|
|
140
|
+
msg: `h1 is ${h.length} chars (soft limit 60): "${h.slice(0, 50)}…"`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const h of heads2) {
|
|
145
|
+
if (h.length > 100) {
|
|
146
|
+
findings.push({
|
|
147
|
+
rule: "long-subtitle",
|
|
148
|
+
severity: "W",
|
|
149
|
+
msg: `h2 is ${h.length} chars (soft limit 100)`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const ref of findImageRefs(body)) {
|
|
154
|
+
const abs = resolve(deckDir, ref);
|
|
155
|
+
if (!(await exists(abs))) {
|
|
156
|
+
findings.push({
|
|
157
|
+
rule: "missing-asset",
|
|
158
|
+
severity: "E",
|
|
159
|
+
msg: `image referenced but not found: ${ref}`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Layout-specific rules ─────────────────────────────────────────────
|
|
165
|
+
switch (layout) {
|
|
166
|
+
case "title":
|
|
167
|
+
case "closing": {
|
|
168
|
+
if (heads1.length === 0) {
|
|
169
|
+
findings.push({
|
|
170
|
+
rule: "title-no-h1",
|
|
171
|
+
severity: "E",
|
|
172
|
+
msg: "missing h1 (deck title)",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "section": {
|
|
178
|
+
if (heads1.length === 0) {
|
|
179
|
+
findings.push({
|
|
180
|
+
rule: "section-no-h1",
|
|
181
|
+
severity: "W",
|
|
182
|
+
msg: "section divider with no h1",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "agenda": {
|
|
188
|
+
const items = countTopLevelListItems(body);
|
|
189
|
+
if (items > 10) {
|
|
190
|
+
findings.push({
|
|
191
|
+
rule: "agenda-overflow",
|
|
192
|
+
severity: "W",
|
|
193
|
+
msg: `${items} items — agenda fits 10 cleanly; split into two slides`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (items === 0) {
|
|
197
|
+
findings.push({
|
|
198
|
+
rule: "agenda-empty",
|
|
199
|
+
severity: "E",
|
|
200
|
+
msg: "agenda layout has no list items",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case "content": {
|
|
206
|
+
const items = countTopLevelListItems(body);
|
|
207
|
+
if (items > 7) {
|
|
208
|
+
findings.push({
|
|
209
|
+
rule: "content-bullets",
|
|
210
|
+
severity: "W",
|
|
211
|
+
msg: `${items} bullets — content slides fit ~7 cleanly`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case "comparison": {
|
|
217
|
+
if (!has(`class="compare"`)) {
|
|
218
|
+
findings.push({
|
|
219
|
+
rule: "comparison-wrapper",
|
|
220
|
+
severity: "E",
|
|
221
|
+
msg: 'missing <div class="compare"> wrapper',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const options = (body.match(/class="option"/g) || []).length;
|
|
225
|
+
if (options === 0) {
|
|
226
|
+
findings.push({
|
|
227
|
+
rule: "comparison-empty",
|
|
228
|
+
severity: "E",
|
|
229
|
+
msg: "no .option blocks found",
|
|
230
|
+
});
|
|
231
|
+
} else if (options > 3) {
|
|
232
|
+
findings.push({
|
|
233
|
+
rule: "comparison-count",
|
|
234
|
+
severity: "W",
|
|
235
|
+
msg: `${options} options — comparison fits 2–3 cleanly`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "metric-grid": {
|
|
241
|
+
if (!has(`class="metrics"`)) {
|
|
242
|
+
findings.push({
|
|
243
|
+
rule: "metric-grid-wrapper",
|
|
244
|
+
severity: "E",
|
|
245
|
+
msg: 'missing <div class="metrics"> wrapper',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const metrics = (body.match(/class="metric"/g) || []).length;
|
|
249
|
+
if (metrics === 0) {
|
|
250
|
+
findings.push({
|
|
251
|
+
rule: "metric-grid-empty",
|
|
252
|
+
severity: "E",
|
|
253
|
+
msg: "no .metric blocks found",
|
|
254
|
+
});
|
|
255
|
+
} else if (metrics !== 3) {
|
|
256
|
+
findings.push({
|
|
257
|
+
rule: "metric-grid-count",
|
|
258
|
+
severity: "W",
|
|
259
|
+
msg: `${metrics} metrics — grid is 3-column, expects exactly 3`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case "two-column": {
|
|
265
|
+
if (!has(`class="col-left"`) || !has(`class="col-right"`)) {
|
|
266
|
+
findings.push({
|
|
267
|
+
rule: "two-column-wrappers",
|
|
268
|
+
severity: "E",
|
|
269
|
+
msg: 'missing <div class="col-left"> or <div class="col-right">',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "image-text": {
|
|
275
|
+
if (!has(`class="image"`) || !has(`class="text"`)) {
|
|
276
|
+
findings.push({
|
|
277
|
+
rule: "image-text-wrappers",
|
|
278
|
+
severity: "E",
|
|
279
|
+
msg: 'missing <div class="image"> or <div class="text">',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case "big-stat": {
|
|
285
|
+
if (heads1.length === 0) {
|
|
286
|
+
findings.push({
|
|
287
|
+
rule: "big-stat-no-h1",
|
|
288
|
+
severity: "E",
|
|
289
|
+
msg: "missing h1 (the stat itself)",
|
|
290
|
+
});
|
|
291
|
+
} else if (heads1.length > 1) {
|
|
292
|
+
findings.push({
|
|
293
|
+
rule: "big-stat-multi-h1",
|
|
294
|
+
severity: "W",
|
|
295
|
+
msg: "multiple h1s — big-stat shows one number",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case "quote":
|
|
301
|
+
case "pull-quote": {
|
|
302
|
+
if (!/^>\s+/m.test(body)) {
|
|
303
|
+
findings.push({
|
|
304
|
+
rule: "quote-no-blockquote",
|
|
305
|
+
severity: "E",
|
|
306
|
+
msg: "no blockquote (`> ...`) found",
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
case "code": {
|
|
312
|
+
const blocks = codeBlockLineCounts(body);
|
|
313
|
+
if (blocks.length === 0) {
|
|
314
|
+
findings.push({
|
|
315
|
+
rule: "code-no-block",
|
|
316
|
+
severity: "E",
|
|
317
|
+
msg: "no fenced code block found",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
for (const n of blocks) {
|
|
321
|
+
if (n > 25) {
|
|
322
|
+
findings.push({
|
|
323
|
+
rule: "code-too-long",
|
|
324
|
+
severity: "W",
|
|
325
|
+
msg: `code block has ${n} lines — fits ~25 before overflow`,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
case "table": {
|
|
332
|
+
const rows = body.split("\n").filter((l) => /^\s*\|.*\|\s*$/.test(l)).length;
|
|
333
|
+
if (rows > 10) {
|
|
334
|
+
findings.push({
|
|
335
|
+
rule: "table-rows",
|
|
336
|
+
severity: "W",
|
|
337
|
+
msg: `${rows} table rows — gets cramped past ~8`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { name: slide.name, layout, findings };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function pad(s: string, n: number): string {
|
|
348
|
+
return s + " ".repeat(Math.max(0, n - s.length));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function main() {
|
|
352
|
+
const argv = process.argv.slice(2);
|
|
353
|
+
if (argv.length === 0) {
|
|
354
|
+
console.error("usage: doctor.ts <deck-dir> [--strict]");
|
|
355
|
+
process.exit(2);
|
|
356
|
+
}
|
|
357
|
+
const deckDir = resolve(argv[0]);
|
|
358
|
+
const strict = argv.includes("--strict");
|
|
359
|
+
|
|
360
|
+
const slides = await loadSlides(deckDir);
|
|
361
|
+
const reports = await Promise.all(slides.map((s) => lintSlide(s, deckDir)));
|
|
362
|
+
|
|
363
|
+
let errors = 0;
|
|
364
|
+
let warnings = 0;
|
|
365
|
+
let printed = false;
|
|
366
|
+
|
|
367
|
+
for (const r of reports) {
|
|
368
|
+
if (r.findings.length === 0) continue;
|
|
369
|
+
if (!printed) {
|
|
370
|
+
console.log(`Doctor — ${basename(deckDir)} (${slides.length} slides)`);
|
|
371
|
+
console.log("");
|
|
372
|
+
printed = true;
|
|
373
|
+
}
|
|
374
|
+
console.log(` ${pad(r.name, 32)} [${r.layout}]`);
|
|
375
|
+
for (const f of r.findings) {
|
|
376
|
+
const sev = f.severity === "E" ? "ERROR" : "WARN ";
|
|
377
|
+
const symbol = f.severity === "E" ? "✗" : "⚠";
|
|
378
|
+
console.log(` ${symbol} ${sev} ${f.rule.padEnd(22)} ${f.msg}`);
|
|
379
|
+
if (f.severity === "E") errors++;
|
|
380
|
+
else warnings++;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const total = errors + warnings;
|
|
385
|
+
if (total === 0) {
|
|
386
|
+
console.log(`✓ Doctor — ${basename(deckDir)} (${slides.length} slides) — all clean`);
|
|
387
|
+
process.exit(0);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
console.log("");
|
|
391
|
+
console.log(
|
|
392
|
+
`Summary: ${errors} error(s), ${warnings} warning(s) across ${slides.length} slides`
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const exitCode = errors > 0 || (strict && warnings > 0) ? 1 : 0;
|
|
396
|
+
process.exit(exitCode);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Only run as a CLI when invoked directly — allows test files to import lintSlide
|
|
400
|
+
// without triggering the argv parsing / process.exit path.
|
|
401
|
+
if (import.meta.main) {
|
|
402
|
+
main().catch((e) => {
|
|
403
|
+
console.error(e?.message ?? e);
|
|
404
|
+
process.exit(2);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { constants as fsConst } from "node:fs";
|
|
8
8
|
import { access, copyFile, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
9
|
-
import { join, resolve } from "node:path";
|
|
9
|
+
import { basename, join, resolve } from "node:path";
|
|
10
10
|
import { SKILL_DEMO, SKILL_TEMPLATE } from "./lib/paths";
|
|
11
11
|
import { listTemplates } from "./lib/registry";
|
|
12
12
|
|
|
@@ -84,7 +84,13 @@ lang: en
|
|
|
84
84
|
await copyFile(join(sourceSlidesDir, f), join(slidesDir, f));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
// If the user happens to run build/present from inside this deck-dir, the output
|
|
88
|
+
// subdir lands here too. Pre-ignore it so it doesn't get accidentally committed.
|
|
89
|
+
const slug =
|
|
90
|
+
basename(target)
|
|
91
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
92
|
+
.replace(/^-+|-+$/g, "") || "deck";
|
|
93
|
+
await writeFile(join(target, ".gitignore"), `${slug}/\n`, "utf8");
|
|
88
94
|
|
|
89
95
|
console.log(`✓ deck scaffolded at ${target}`);
|
|
90
96
|
console.log(` template: ${templateName}`);
|
|
@@ -93,6 +99,7 @@ lang: en
|
|
|
93
99
|
console.log(`\nNext:`);
|
|
94
100
|
console.log(` $EDITOR ${slidesDir}/`);
|
|
95
101
|
console.log(` bun ~/.pal/skills/presentation/tools/build.ts ${target}`);
|
|
102
|
+
console.log(` # output → <cwd>/${slug}/${slug}.html (override with --out <dir>)`);
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
main().catch((e) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"lint-write": "biome lint --write",
|
|
41
41
|
"check": "biome check",
|
|
42
42
|
"check-write": "biome check --write",
|
|
43
|
+
"knip": "knip-bun",
|
|
43
44
|
"lint-staged": "lint-staged",
|
|
44
45
|
"prepare": "bun .husky/install.mjs",
|
|
45
46
|
"install:all": "bun run src/cli/index.ts cli install",
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
"@types/bun": "latest",
|
|
71
72
|
"@types/node": "latest",
|
|
72
73
|
"husky": "^9.1.7",
|
|
74
|
+
"knip": "^6.9.0",
|
|
73
75
|
"lint-staged": "^15.5.0",
|
|
74
76
|
"semantic-release": "^25.0.3",
|
|
75
77
|
"typescript": "^5.9.0"
|
package/src/targets/lib.ts
CHANGED
|
@@ -447,10 +447,7 @@ type AgentPlatform = (typeof AGENT_PLATFORMS)[number];
|
|
|
447
447
|
* The target platform block is un-indented and merged into the root.
|
|
448
448
|
* All other platform blocks are stripped.
|
|
449
449
|
*/
|
|
450
|
-
|
|
451
|
-
content: string,
|
|
452
|
-
platform: AgentPlatform
|
|
453
|
-
): string {
|
|
450
|
+
function extractAgentForPlatform(content: string, platform: AgentPlatform): string {
|
|
454
451
|
const parts = content.split(/^---\s*$/m);
|
|
455
452
|
if (parts.length < 3) return content;
|
|
456
453
|
|
|
@@ -1,70 +0,0 @@
|
|
|
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
|
-
});
|