@hajimism/dek 0.3.3 → 0.5.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/CHANGELOG.md +136 -0
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/package.json +5 -3
- package/skills/dek/SKILL.md +10 -3
- package/skills/dek-design/SKILL.md +42 -0
- package/src/cli/address.ts +95 -18
- package/src/cli/build.ts +2 -2
- package/src/cli/check.ts +2 -1
- package/src/cli/commands.ts +57 -19
- package/src/cli/contract.ts +58 -4
- package/src/cli/files.ts +2 -5
- package/src/cli/flags.ts +1 -1
- package/src/cli/format.ts +5 -4
- package/src/cli/goto.ts +44 -11
- package/src/cli/init.ts +10 -5
- package/src/cli/lint.ts +36 -16
- package/src/cli/ls.ts +5 -1
- package/src/cli/main.ts +51 -14
- package/src/cli/new.ts +28 -8
- package/src/cli/pptx.ts +9 -4
- package/src/cli/ref.ts +92 -9
- package/src/cli/result.ts +47 -6
- package/src/cli/scope.ts +57 -29
- package/src/cli/shot.ts +78 -6
- package/src/cli/show.ts +15 -2
- package/src/cli/text.ts +88 -11
- package/src/cli/theme.ts +58 -10
- package/src/cli/tty.ts +16 -2
- package/src/cli/voice.ts +15 -9
- package/src/core/alignment.ts +320 -0
- package/src/core/annotation-rows.ts +16 -4
- package/src/core/annotations.ts +19 -1
- package/src/core/assets.ts +18 -10
- package/src/core/attribute-value.ts +188 -0
- package/src/core/build.ts +3 -3
- package/src/core/config.ts +61 -12
- package/src/core/css-scan.ts +173 -39
- package/src/core/css-transform.ts +57 -11
- package/src/core/css.ts +19 -13
- package/src/core/diagnostic.ts +5 -1
- package/src/core/escape.ts +43 -0
- package/src/core/foreign-text.ts +138 -0
- package/src/core/github.ts +1 -0
- package/src/core/html-entities.ts +318 -0
- package/src/core/html-scan.ts +49 -27
- package/src/core/html.ts +21 -15
- package/src/core/line-break.ts +275 -0
- package/src/core/lint/asset-refs.ts +50 -8
- package/src/core/lint/files.ts +30 -5
- package/src/core/lint/keys.ts +7 -3
- package/src/core/lint/slide-html.ts +24 -19
- package/src/core/lint/slide-script.ts +7 -5
- package/src/core/mv.ts +57 -27
- package/src/core/order.ts +14 -0
- package/src/core/pack.ts +2 -2
- package/src/core/path.ts +8 -2
- package/src/core/playwright-module.d.ts +13 -1
- package/src/core/playwright-motion.ts +1 -0
- package/src/core/playwright-sheet.ts +1 -0
- package/src/core/playwright-visual.ts +207 -30
- package/src/core/playwright.ts +279 -44
- package/src/core/pptx-package.ts +461 -33
- package/src/core/pptx-paint.ts +1168 -0
- package/src/core/pptx-plan.ts +879 -0
- package/src/core/pptx-text.ts +204 -47
- package/src/core/pptx-theme.ts +222 -0
- package/src/core/pptx.ts +56 -8
- package/src/core/prose.ts +23 -0
- package/src/core/pseudo-text.ts +32 -10
- package/src/core/ref-name.ts +52 -8
- package/src/core/ref.ts +308 -54
- package/src/core/resolve.ts +23 -11
- package/src/core/rumdl.ts +51 -25
- package/src/core/safe-fs.ts +19 -2
- package/src/core/sandbox.ts +217 -0
- package/src/core/shot/morph.ts +1 -1
- package/src/core/shot/motion.ts +2 -3
- package/src/core/shot/still.ts +2 -2
- package/src/core/shot-cache.ts +26 -17
- package/src/core/slide-eval-worker.ts +1 -0
- package/src/core/slide-measure.ts +51 -1
- package/src/core/slide-script-eval.ts +12 -8
- package/src/core/slide-script.ts +114 -20
- package/src/core/spawn.ts +131 -32
- package/src/core/sync.ts +5 -14
- package/src/core/tar.ts +315 -0
- package/src/core/text-contrast.ts +2 -0
- package/src/core/toml-keys.ts +116 -51
- package/src/core/url-attributes.ts +27 -9
- package/src/core/visual-cache.ts +2 -0
- package/src/core/visual.ts +182 -24
- package/src/core/voice.ts +5 -6
- package/src/core/zod.ts +9 -1
- package/src/server/auth.ts +21 -0
- package/src/server/dev.ts +26 -7
- package/src/server/lock.ts +50 -7
- package/src/server/routes.ts +7 -2
- package/src/server/serial.ts +8 -2
- package/src/theme/default.css +4 -0
- package/src/video/bake.ts +6 -2
- package/src/video/mux.ts +27 -3
- package/src/video/recorder.ts +3 -2
- package/src/video/worker.ts +1 -0
- package/src/voice/engine.ts +81 -18
package/src/cli/commands.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { hasErrors } from "../core/diagnostic.ts";
|
|
2
|
+
import { shellQuote } from "../core/escape.ts";
|
|
3
|
+
import type { HiddenText } from "../core/foreign-text.ts";
|
|
2
4
|
import { isRefName } from "../core/ref-name.ts";
|
|
3
5
|
import { mergeSarif, toSarif } from "../core/sarif.ts";
|
|
4
6
|
import type { CheckCliResult } from "./check.ts";
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
formatBuild,
|
|
15
17
|
formatCheck,
|
|
16
18
|
formatCues,
|
|
19
|
+
formatHidden,
|
|
17
20
|
formatInit,
|
|
18
21
|
formatKept,
|
|
19
22
|
formatKeptTheme,
|
|
@@ -24,6 +27,7 @@ import {
|
|
|
24
27
|
formatNew,
|
|
25
28
|
formatPack,
|
|
26
29
|
formatPdf,
|
|
30
|
+
formatPptx,
|
|
27
31
|
formatRef,
|
|
28
32
|
formatReport,
|
|
29
33
|
formatShot,
|
|
@@ -63,14 +67,17 @@ type AnyCtx = Omit<Ctx<[], DeckScope | undefined>, "args"> & {
|
|
|
63
67
|
/** How a command's result is printed. Only `text` is required. */
|
|
64
68
|
export type Output<D> = {
|
|
65
69
|
text(data: D, color: boolean): string;
|
|
66
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* What did not happen, or what is worth a look, such as what a ref hides, for stderr next to the
|
|
72
|
+
* text: the result on stdout stays the result.
|
|
73
|
+
*/
|
|
67
74
|
notes?(data: D, color: boolean): string | undefined;
|
|
68
75
|
/** The `--json` fields next to `ok`; the data as-is when absent. */
|
|
69
76
|
json?(data: D): object;
|
|
70
77
|
/** The data with every path into the source tree as `display` shows it. */
|
|
71
78
|
paths?(data: D, display: (path: string) => string): D;
|
|
72
|
-
/** The error of a run that did its job and found one. */
|
|
73
|
-
failure?(data: D): FormattedError | undefined;
|
|
79
|
+
/** The error of a run that did its job and found one; `flags` are the run's, for its rerun. */
|
|
80
|
+
failure?(data: D, flags: FlagValues): FormattedError | undefined;
|
|
74
81
|
/** `--format sarif`. */
|
|
75
82
|
sarif?(data: D): object;
|
|
76
83
|
};
|
|
@@ -192,6 +199,20 @@ function withDisplayDiagnostics<D extends { diagnostics: CheckCliResult["diagnos
|
|
|
192
199
|
return { ...data, diagnostics: displayDiagnostics(data.diagnostics, display) };
|
|
193
200
|
}
|
|
194
201
|
|
|
202
|
+
/** What to do about the characters a person cannot see that a ref's theme brought into a new deck. */
|
|
203
|
+
const COPIED_HIDDEN_HELP =
|
|
204
|
+
"they came in with the ref's theme, and a person reading the deck sees none of them; remove them from theme.css unless you mean them";
|
|
205
|
+
|
|
206
|
+
/** The data with each hidden-character report's path as `display` shows it. */
|
|
207
|
+
function withDisplayHidden<D extends { hidden?: HiddenText[] }>(
|
|
208
|
+
data: D,
|
|
209
|
+
display: (path: string) => string,
|
|
210
|
+
): D {
|
|
211
|
+
return data.hidden === undefined
|
|
212
|
+
? data
|
|
213
|
+
: { ...data, hidden: data.hidden.map((entry) => ({ ...entry, path: display(entry.path) })) };
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
/**
|
|
196
217
|
* Every command, in the order `dekc help` lists them: the words and flags it takes, its help,
|
|
197
218
|
* what it works on, how it runs, and how its result prints. Each module loads only when its
|
|
@@ -287,11 +308,12 @@ export const COMMANDS = {
|
|
|
287
308
|
},
|
|
288
309
|
output: {
|
|
289
310
|
text: formatNew,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
311
|
+
notes: (data, color) => formatHidden(data.hidden, color, COPIED_HIDDEN_HELP) || undefined,
|
|
312
|
+
paths: (data, display) =>
|
|
313
|
+
withDisplayHidden(
|
|
314
|
+
{ ...data, created: data.created.map(display), updated: data.updated.map(display) },
|
|
315
|
+
display,
|
|
316
|
+
),
|
|
295
317
|
},
|
|
296
318
|
}),
|
|
297
319
|
ls: result({
|
|
@@ -310,10 +332,12 @@ export const COMMANDS = {
|
|
|
310
332
|
},
|
|
311
333
|
output: {
|
|
312
334
|
text: formatLs,
|
|
335
|
+
notes: (data, color) =>
|
|
336
|
+
(data.kind === "deck" && formatHidden(data.hidden, color)) || undefined,
|
|
313
337
|
json: ({ kind: _kind, ...data }) => data,
|
|
314
338
|
paths: (data, display) =>
|
|
315
339
|
data.kind === "deck"
|
|
316
|
-
? withDisplayDiagnostics(data, display)
|
|
340
|
+
? withDisplayHidden(withDisplayDiagnostics(data, display), display)
|
|
317
341
|
: {
|
|
318
342
|
...withDisplayDiagnostics(data, display),
|
|
319
343
|
decks: data.decks.map((deck) => withDisplayDiagnostics(deck, display)),
|
|
@@ -397,7 +421,11 @@ export const COMMANDS = {
|
|
|
397
421
|
const { showCommand } = await import("./show.ts");
|
|
398
422
|
return showCommand(target, args.slug);
|
|
399
423
|
},
|
|
400
|
-
output: {
|
|
424
|
+
output: {
|
|
425
|
+
text: formatShow,
|
|
426
|
+
notes: (data, color) => formatHidden(data.hidden, color) || undefined,
|
|
427
|
+
paths: withDisplayHidden,
|
|
428
|
+
},
|
|
401
429
|
}),
|
|
402
430
|
theme: result({
|
|
403
431
|
scope: "deck",
|
|
@@ -422,7 +450,8 @@ export const COMMANDS = {
|
|
|
422
450
|
},
|
|
423
451
|
output: {
|
|
424
452
|
text: formatTheme,
|
|
425
|
-
|
|
453
|
+
notes: (data, color) => formatHidden(data.hidden, color) || undefined,
|
|
454
|
+
paths: (data, display) => withDisplayHidden({ ...data, path: display(data.path) }, display),
|
|
426
455
|
},
|
|
427
456
|
}),
|
|
428
457
|
check: result({
|
|
@@ -444,7 +473,7 @@ export const COMMANDS = {
|
|
|
444
473
|
text: formatCheck,
|
|
445
474
|
notes: (data, color) => formatSkipped(data.skipped, color) || undefined,
|
|
446
475
|
paths: withDisplayDiagnostics,
|
|
447
|
-
failure: (data) => diagnosticFailure("check", data, `dekc check ${data.slug}`),
|
|
476
|
+
failure: (data) => diagnosticFailure("check", data, `dekc check ${shellQuote(data.slug)}`),
|
|
448
477
|
},
|
|
449
478
|
}),
|
|
450
479
|
shot: result({
|
|
@@ -484,7 +513,11 @@ export const COMMANDS = {
|
|
|
484
513
|
motion: flags.motion,
|
|
485
514
|
});
|
|
486
515
|
},
|
|
487
|
-
output: {
|
|
516
|
+
output: {
|
|
517
|
+
text: formatShot,
|
|
518
|
+
notes: (data, color) => formatHidden(data.hidden, color) || undefined,
|
|
519
|
+
paths: withDisplayHidden,
|
|
520
|
+
},
|
|
488
521
|
}),
|
|
489
522
|
mv: result({
|
|
490
523
|
scope: "deck",
|
|
@@ -659,7 +692,7 @@ export const COMMANDS = {
|
|
|
659
692
|
group: "Output",
|
|
660
693
|
overview: [
|
|
661
694
|
["dekc lint [--fix]", "check script.md against slides; --fix syncs first"],
|
|
662
|
-
["dekc lint --visual", "add overflow and
|
|
695
|
+
["dekc lint --visual", "add overflow, contrast, line-break, and alignment rules"],
|
|
663
696
|
["dekc lint --format sarif", "print diagnostics as SARIF, for code scanning"],
|
|
664
697
|
],
|
|
665
698
|
agent: ["dekc lint [--fix] [--visual] [--format sarif]"],
|
|
@@ -672,7 +705,9 @@ export const COMMANDS = {
|
|
|
672
705
|
notes: (data, color) => formatSkipped(data.skipped, color) || undefined,
|
|
673
706
|
json: ({ rumdlSarif: _rumdlSarif, ...data }) => data,
|
|
674
707
|
paths: withDisplayDiagnostics,
|
|
675
|
-
|
|
708
|
+
// The rerun measures what this run did: without --visual, the errors it found may not show.
|
|
709
|
+
failure: (data, flags) =>
|
|
710
|
+
diagnosticFailure("lint", data, flags.visual ? "dekc lint --visual" : "dekc lint"),
|
|
676
711
|
sarif: (data) => {
|
|
677
712
|
const dek = data.diagnostics.filter((diagnostic) => diagnostic.id.startsWith("DEK"));
|
|
678
713
|
return mergeSarif(toSarif(dek, { skipped: data.skipped ?? [] }), data.rumdlSarif);
|
|
@@ -827,19 +862,22 @@ export const COMMANDS = {
|
|
|
827
862
|
flags: ["root-dist", "public"],
|
|
828
863
|
usage: ["dekc pptx [deck] [--root-dist] [--public]"],
|
|
829
864
|
summary:
|
|
830
|
-
"Write dist/<deck>.pptx, one slide per slide at its last beat: a picture of
|
|
865
|
+
"Write dist/<deck>.pptx, one slide per slide at its last beat: its plain boxes, pictures, and\ntext as PowerPoint's own over a picture of the rest, in the deck's theme, and its script in\nthe notes. Needs Playwright.",
|
|
831
866
|
group: "Output",
|
|
832
867
|
overview: [
|
|
833
|
-
[
|
|
868
|
+
[
|
|
869
|
+
"dekc pptx [--root-dist] [--public]",
|
|
870
|
+
"write a PowerPoint file with editable shapes and text",
|
|
871
|
+
],
|
|
834
872
|
],
|
|
835
873
|
agent: [
|
|
836
|
-
"dekc pptx [--root-dist] [--public] text editable over a picture of
|
|
874
|
+
"dekc pptx [--root-dist] [--public] boxes, pictures, and text editable over a picture of the rest; script in notes",
|
|
837
875
|
],
|
|
838
876
|
run: async ({ target, flags }) => {
|
|
839
877
|
const { pptxCommand } = await import("./pptx.ts");
|
|
840
878
|
return pptxCommand(target, { rootDist: flags["root-dist"], public: flags.public });
|
|
841
879
|
},
|
|
842
|
-
output: { text:
|
|
880
|
+
output: { text: formatPptx, json: ({ lifted: _lifted, ...data }) => data },
|
|
843
881
|
}),
|
|
844
882
|
help: {
|
|
845
883
|
kind: "help",
|
package/src/cli/contract.ts
CHANGED
|
@@ -50,6 +50,19 @@ const SkippedCheck = z
|
|
|
50
50
|
description: "A check that did not run, so an empty list of findings is no pass for it.",
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
const Unsandboxed = z
|
|
54
|
+
.strictObject({
|
|
55
|
+
reason: z.string().describe("What Chromium said when it did not start with its sandbox."),
|
|
56
|
+
hint: z
|
|
57
|
+
.string()
|
|
58
|
+
.describe("What lets the sandbox start, and, unless already set, DEK_ALLOW_UNSANDBOXED=1."),
|
|
59
|
+
})
|
|
60
|
+
.meta({
|
|
61
|
+
id: "Unsandboxed",
|
|
62
|
+
description:
|
|
63
|
+
"Chromium drew without its sandbox, which did not start here, as in a container running as root. A ref is never drawn so unless DEK_ALLOW_UNSANDBOXED=1.",
|
|
64
|
+
});
|
|
65
|
+
|
|
53
66
|
const Failure = z
|
|
54
67
|
.strictObject({
|
|
55
68
|
message: z.string(),
|
|
@@ -68,6 +81,27 @@ const RefInfo = z
|
|
|
68
81
|
})
|
|
69
82
|
.meta({ id: "RefInfo", description: "Set when the deck read is a ref." });
|
|
70
83
|
|
|
84
|
+
const HiddenText = z
|
|
85
|
+
.strictObject({
|
|
86
|
+
path: z.string(),
|
|
87
|
+
line: z.number().int(),
|
|
88
|
+
column: z
|
|
89
|
+
.number()
|
|
90
|
+
.int()
|
|
91
|
+
.describe("1-based, in UTF-16 units as editors count: where the first of them is."),
|
|
92
|
+
codePoints: z.array(z.string()).describe("Each of them on the line, in order, as U+XXXX."),
|
|
93
|
+
})
|
|
94
|
+
.meta({
|
|
95
|
+
id: "HiddenText",
|
|
96
|
+
description:
|
|
97
|
+
"A line of a ref's file that holds characters a person cannot see: those Unicode marks default-ignorable, such as tag characters, zero-width characters, the byte order mark, bidi controls, and Hangul fillers, but not variation selectors; the line and paragraph separators; and the interlinear annotation marks. Text output shows each as ⟨U+XXXX⟩.",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const hidden = z
|
|
101
|
+
.array(HiddenText)
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("A ref's lines that hold characters a person cannot see; absent when none does.");
|
|
104
|
+
|
|
71
105
|
const Position = z
|
|
72
106
|
.strictObject({ slideIndex: z.number().int(), beatIndex: z.number().int() })
|
|
73
107
|
.meta({ id: "Position" });
|
|
@@ -75,6 +109,9 @@ const Position = z
|
|
|
75
109
|
const paths = z.array(z.string());
|
|
76
110
|
const diagnostics = z.array(Diagnostic);
|
|
77
111
|
const skipped = z.array(SkippedCheck).optional().describe("Absent when every check ran.");
|
|
112
|
+
const unsandboxed = Unsandboxed.optional().describe(
|
|
113
|
+
"Absent when every page drawn was drawn in Chromium's sandbox, or none was.",
|
|
114
|
+
);
|
|
78
115
|
|
|
79
116
|
const nav = z.strictObject({
|
|
80
117
|
slug: z.string(),
|
|
@@ -112,6 +149,7 @@ export const RESULT_FIELDS = {
|
|
|
112
149
|
updated: paths,
|
|
113
150
|
next: z.array(z.string()),
|
|
114
151
|
warnings: z.array(z.string()).describe("Set when the theme came from a ref with no license."),
|
|
152
|
+
hidden,
|
|
115
153
|
}),
|
|
116
154
|
ls: z.union([
|
|
117
155
|
z.strictObject({
|
|
@@ -149,6 +187,7 @@ export const RESULT_FIELDS = {
|
|
|
149
187
|
diagnostics,
|
|
150
188
|
skipped,
|
|
151
189
|
ref: RefInfo.optional(),
|
|
190
|
+
hidden,
|
|
152
191
|
}),
|
|
153
192
|
]),
|
|
154
193
|
ref: z.union([
|
|
@@ -202,6 +241,7 @@ export const RESULT_FIELDS = {
|
|
|
202
241
|
),
|
|
203
242
|
assets: paths,
|
|
204
243
|
ref: RefInfo.optional(),
|
|
244
|
+
hidden,
|
|
205
245
|
}),
|
|
206
246
|
theme: z.strictObject({
|
|
207
247
|
path: z.string(),
|
|
@@ -214,14 +254,24 @@ export const RESULT_FIELDS = {
|
|
|
214
254
|
"State classes the theme selects that the player sets at runtime (is-current, is-shown); never written in markup.",
|
|
215
255
|
),
|
|
216
256
|
tokens: z.array(z.strictObject({ name: z.string(), value: z.string() })),
|
|
217
|
-
layouts: z
|
|
257
|
+
layouts: z
|
|
258
|
+
.array(z.strictObject({ name: z.string(), example: z.string().optional() }))
|
|
259
|
+
.describe("The layouts whose names are plain words, which dek prints."),
|
|
260
|
+
unprintedLayouts: z
|
|
261
|
+
.number()
|
|
262
|
+
.optional()
|
|
263
|
+
.describe(
|
|
264
|
+
"How many layouts the theme has whose names are no plain word, left out of layouts; absent when none.",
|
|
265
|
+
),
|
|
218
266
|
layout: z.strictObject({ name: z.string(), example: z.string() }).optional(),
|
|
219
267
|
ref: RefInfo.optional(),
|
|
268
|
+
hidden,
|
|
220
269
|
}),
|
|
221
270
|
check: z.strictObject({
|
|
222
271
|
slug: z.string(),
|
|
223
272
|
diagnostics,
|
|
224
273
|
skipped,
|
|
274
|
+
unsandboxed,
|
|
225
275
|
shot: z.string().optional(),
|
|
226
276
|
fill: z
|
|
227
277
|
.strictObject({
|
|
@@ -281,6 +331,8 @@ export const RESULT_FIELDS = {
|
|
|
281
331
|
)
|
|
282
332
|
.optional()
|
|
283
333
|
.describe("With --motion, each beat in order with its frames, the settled end last."),
|
|
334
|
+
unsandboxed,
|
|
335
|
+
hidden,
|
|
284
336
|
}),
|
|
285
337
|
mv: z.strictObject({
|
|
286
338
|
from: z.string(),
|
|
@@ -370,7 +422,7 @@ export const RESULT_FIELDS = {
|
|
|
370
422
|
z.strictObject({ action: z.literal("clear"), cleared: z.number().int() }),
|
|
371
423
|
]),
|
|
372
424
|
sync: z.strictObject({ created: paths, updated: paths, removed: paths, kept: paths }),
|
|
373
|
-
lint: z.strictObject({ diagnostics, skipped }),
|
|
425
|
+
lint: z.strictObject({ diagnostics, skipped, unsandboxed }),
|
|
374
426
|
cues: z.strictObject({
|
|
375
427
|
name: z.string(),
|
|
376
428
|
cues: z.array(
|
|
@@ -409,15 +461,17 @@ export const RESULT_FIELDS = {
|
|
|
409
461
|
images: paths.describe("dist/<deck>.png for each deck that got a link preview image."),
|
|
410
462
|
diagnostics,
|
|
411
463
|
skipped,
|
|
464
|
+
unsandboxed,
|
|
412
465
|
}),
|
|
413
466
|
video: z.strictObject({
|
|
414
467
|
out: z.string(),
|
|
415
468
|
vtt: z.string().optional(),
|
|
416
469
|
chapters: z.string().optional(),
|
|
417
470
|
credits: z.string().optional(),
|
|
471
|
+
unsandboxed,
|
|
418
472
|
}),
|
|
419
|
-
pdf: z.strictObject({ outs: paths }),
|
|
420
|
-
pptx: z.strictObject({ outs: paths }),
|
|
473
|
+
pdf: z.strictObject({ outs: paths, unsandboxed }),
|
|
474
|
+
pptx: z.strictObject({ outs: paths, unsandboxed }),
|
|
421
475
|
pack: z.strictObject({
|
|
422
476
|
outs: paths,
|
|
423
477
|
warnings: z.array(z.string()).describe("Once per project: the packs carry no license."),
|
package/src/cli/files.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
// biome-ignore lint/style/noRestrictedImports: mkdirSync and writeFileSync, for init and new, which create a file only where none is (wx); safe-fs replaces one.
|
|
1
2
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { dirname, join, relative } from "node:path";
|
|
3
4
|
import { bundledTheme } from "../core/bundled-theme.ts";
|
|
4
5
|
import type { DekConfig } from "../core/config.ts";
|
|
5
6
|
import { deckPaths } from "../core/deck-paths.ts";
|
|
6
7
|
import { DekError } from "../core/error.ts";
|
|
8
|
+
import { shellQuote } from "../core/escape.ts";
|
|
7
9
|
import { walkUp } from "../core/optional.ts";
|
|
8
10
|
import { PLAYWRIGHT_INSTALL } from "../core/playwright.ts";
|
|
9
11
|
import { readSourceIfExists, writeInside } from "../core/safe-fs.ts";
|
|
@@ -316,8 +318,3 @@ function inNodeModules(root: string, ...path: string[]): boolean {
|
|
|
316
318
|
walkUp(root, (dir) => existsSync(join(dir, "node_modules", ...path)) || undefined) === true
|
|
317
319
|
);
|
|
318
320
|
}
|
|
319
|
-
|
|
320
|
-
/** `word` as one shell word: as it is when that is safe, else single-quoted. */
|
|
321
|
-
export function shellQuote(word: string): string {
|
|
322
|
-
return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
|
|
323
|
-
}
|
package/src/cli/flags.ts
CHANGED
|
@@ -41,7 +41,7 @@ export const FLAGS = {
|
|
|
41
41
|
fix: { type: "boolean", text: "create missing skeleton slides before linting" },
|
|
42
42
|
visual: {
|
|
43
43
|
type: "boolean",
|
|
44
|
-
text: "add the rendered rules: overflow and
|
|
44
|
+
text: "add the rendered rules: overflow, contrast, line breaks, and alignment (needs Playwright)",
|
|
45
45
|
},
|
|
46
46
|
shot: { type: "boolean", text: "also write a screenshot and return its path" },
|
|
47
47
|
voice: { type: "boolean", text: "also return each sentence's reading and duration" },
|
package/src/cli/format.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { isAbsolute, relative } from "node:path";
|
|
|
2
2
|
import type { Diagnostic } from "../core/diagnostic.ts";
|
|
3
3
|
import { type ErrorFields, errorFields } from "../core/error.ts";
|
|
4
4
|
import type { LiveEvent } from "../core/live-protocol.ts";
|
|
5
|
-
import { ansi, displayWidth, padEndWidth, padStartWidth } from "./tty.ts";
|
|
5
|
+
import { ansi, displayWidth, padEndWidth, padStartWidth, plainText } from "./tty.ts";
|
|
6
6
|
|
|
7
7
|
function formatLocation(location: { path?: string; line?: number; column?: number }): string {
|
|
8
8
|
if (location.path !== undefined && location.line !== undefined) {
|
|
@@ -56,17 +56,18 @@ export function formatError(error: unknown, opts?: { cwd?: string }): FormattedE
|
|
|
56
56
|
: { ...fields, path: displayPath(fields.path, opts?.cwd) };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** An error as the terminal shows it: its own text plain, and dek's colors around it. */
|
|
59
60
|
export function formatErrorText(error: unknown, opts?: { color?: boolean; cwd?: string }): string {
|
|
60
61
|
const formatted = formatError(error, opts);
|
|
61
62
|
const c = ansi(opts?.color === true);
|
|
62
|
-
const parts = [`${c.bold(c.red("error:"))} ${formatted.message}`];
|
|
63
|
+
const parts = [`${c.bold(c.red("error:"))} ${plainText(formatted.message)}`];
|
|
63
64
|
if (formatted.path) {
|
|
64
65
|
const loc =
|
|
65
66
|
formatted.line !== undefined ? `${formatted.path}:${formatted.line}` : formatted.path;
|
|
66
|
-
parts.push(` ${c.cyan("-->")} ${loc}`);
|
|
67
|
+
parts.push(` ${c.cyan("-->")} ${plainText(loc)}`);
|
|
67
68
|
}
|
|
68
69
|
if (formatted.hint) {
|
|
69
|
-
parts.push(` ${c.yellow("help:")} ${formatted.hint}`);
|
|
70
|
+
parts.push(` ${c.yellow("help:")} ${plainText(formatted.hint)}`);
|
|
70
71
|
}
|
|
71
72
|
return parts.join("\n");
|
|
72
73
|
}
|
package/src/cli/goto.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DekError } from "../core/error.ts";
|
|
2
2
|
import { deckRoutePath } from "../runtime/routes.ts";
|
|
3
3
|
import { isPidAlive, readDevServerLock } from "../server/lock.ts";
|
|
4
|
+
import { deckFlag, deckWord } from "./address.ts";
|
|
5
|
+
import { isTypedCommand } from "./commands.ts";
|
|
4
6
|
import { type DeckTarget, requireSection } from "./scope.ts";
|
|
5
7
|
|
|
6
8
|
export type NavResult = {
|
|
@@ -11,22 +13,42 @@ export type NavResult = {
|
|
|
11
13
|
viewers: number;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
type NavOptions = {
|
|
17
|
+
/** Covers the whole request, from connecting to the last byte of the answer. */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** The dev server answers a nav request at once; a stall this long is a server that is stuck. */
|
|
22
|
+
const NAV_TIMEOUT_MS = 10 * 1000;
|
|
23
|
+
|
|
24
|
+
export async function gotoCommand(
|
|
25
|
+
target: DeckTarget,
|
|
26
|
+
slug: string,
|
|
27
|
+
options: NavOptions = {},
|
|
28
|
+
): Promise<NavResult> {
|
|
15
29
|
requireSection(target.deck, slug);
|
|
16
|
-
return requestDevServer(
|
|
17
|
-
|
|
18
|
-
path: "goto",
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
return requestDevServer(
|
|
31
|
+
target,
|
|
32
|
+
{ method: "POST", path: "goto", body: { slug } },
|
|
33
|
+
options.timeoutMs ?? NAV_TIMEOUT_MS,
|
|
34
|
+
);
|
|
21
35
|
}
|
|
22
36
|
|
|
23
|
-
export async function currentCommand(
|
|
24
|
-
|
|
37
|
+
export async function currentCommand(
|
|
38
|
+
target: DeckTarget,
|
|
39
|
+
options: NavOptions = {},
|
|
40
|
+
): Promise<NavResult> {
|
|
41
|
+
return requestDevServer(
|
|
42
|
+
target,
|
|
43
|
+
{ method: "GET", path: "current" },
|
|
44
|
+
options.timeoutMs ?? NAV_TIMEOUT_MS,
|
|
45
|
+
);
|
|
25
46
|
}
|
|
26
47
|
|
|
27
48
|
async function requestDevServer(
|
|
28
49
|
{ project, deck }: DeckTarget,
|
|
29
50
|
request: { method: "GET" | "POST"; path: "goto" | "current"; body?: { slug: string } },
|
|
51
|
+
timeoutMs: number,
|
|
30
52
|
): Promise<NavResult> {
|
|
31
53
|
const deckName = deck.name;
|
|
32
54
|
const lock = readDevServerLock(project.root);
|
|
@@ -43,16 +65,27 @@ async function requestDevServer(
|
|
|
43
65
|
if (lock.password) {
|
|
44
66
|
headers.authorization = `Basic ${btoa(`:${lock.password}`)}`;
|
|
45
67
|
}
|
|
68
|
+
// A server that takes the connection but never answers is stuck; say so instead of waiting.
|
|
69
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
70
|
+
const unlessStuck = (error: unknown): never => {
|
|
71
|
+
throw signal.aborted
|
|
72
|
+
? new DekError(`the dev server at ${lock.url} did not answer within ${timeoutMs / 1000}s`, {
|
|
73
|
+
hint: "restart it: stop `dekc` and run it again",
|
|
74
|
+
})
|
|
75
|
+
: error;
|
|
76
|
+
};
|
|
77
|
+
// biome-ignore lint/style/noRestrictedGlobals: signal, an AbortSignal.timeout, bounds the request and the read of its answer.
|
|
46
78
|
const response = await fetch(url, {
|
|
47
79
|
method: request.method,
|
|
48
80
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
49
81
|
body: request.body ? JSON.stringify(request.body) : undefined,
|
|
50
|
-
|
|
82
|
+
signal,
|
|
83
|
+
}).catch(unlessStuck);
|
|
51
84
|
// Only the nav routes answer JSON; a 404, 401, or 405 from the server itself is plain text.
|
|
52
85
|
if (!response.headers.get("content-type")?.includes("application/json")) {
|
|
53
86
|
throw refusal(response.status, lock, deckName, request.path);
|
|
54
87
|
}
|
|
55
|
-
const json = (await response.json()) as {
|
|
88
|
+
const json = (await response.json().catch(unlessStuck)) as {
|
|
56
89
|
ok?: boolean;
|
|
57
90
|
slug?: string;
|
|
58
91
|
slideIndex?: number;
|
|
@@ -82,7 +115,7 @@ function refusal(
|
|
|
82
115
|
): DekError {
|
|
83
116
|
if (status === 404 && lock.deck !== undefined && lock.deck !== deckName) {
|
|
84
117
|
return new DekError(`the running dev server serves deck "${lock.deck}", not "${deckName}"`, {
|
|
85
|
-
hint: `pass
|
|
118
|
+
hint: `pass ${deckFlag(lock.deck)}, or stop the server at ${lock.url} and run \`dekc ${isTypedCommand(deckName) ? deckFlag(deckName) : deckWord(deckName)}\``,
|
|
86
119
|
});
|
|
87
120
|
}
|
|
88
121
|
if (status === 401) {
|
package/src/cli/init.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { basename, join, resolve } from "node:path";
|
|
|
3
3
|
import { parseCss, publishedTokens } from "../core/css.ts";
|
|
4
4
|
import { deckPaths } from "../core/deck-paths.ts";
|
|
5
5
|
import { DekError } from "../core/error.ts";
|
|
6
|
+
import { shellQuote } from "../core/escape.ts";
|
|
6
7
|
import { REQUIRED_TOKENS } from "../core/lint/tokens.ts";
|
|
7
8
|
import { walkUp } from "../core/optional.ts";
|
|
8
9
|
import { DECK_NAME_HINT, isDeckName } from "../core/path.ts";
|
|
9
|
-
import {
|
|
10
|
+
import { refuseInsideRef } from "../core/ref.ts";
|
|
11
|
+
import { readSourceIfExists } from "../core/safe-fs.ts";
|
|
10
12
|
import {
|
|
11
13
|
defaultTsconfig,
|
|
12
14
|
dekFilePaths,
|
|
@@ -25,7 +27,6 @@ import {
|
|
|
25
27
|
nextSteps,
|
|
26
28
|
type PlannedPath,
|
|
27
29
|
playwrightStep,
|
|
28
|
-
shellQuote,
|
|
29
30
|
} from "./files.ts";
|
|
30
31
|
|
|
31
32
|
export type InitResult = {
|
|
@@ -84,7 +85,7 @@ export function initCommand(options: { cwd: string; dir?: string; deck?: string
|
|
|
84
85
|
/** The tokens every deck needs that the project's theme.css does not publish on `.slide`. */
|
|
85
86
|
function missingThemeTokens(root: string): string[] {
|
|
86
87
|
const published = new Set(
|
|
87
|
-
publishedTokens(parseCss(
|
|
88
|
+
publishedTokens(parseCss(readSourceIfExists(join(root, "theme.css"), root) ?? "")).map(
|
|
88
89
|
(token) => token.name,
|
|
89
90
|
),
|
|
90
91
|
);
|
|
@@ -100,8 +101,10 @@ function syncDeckFiles(deckDir: string, created: string[]): FileChanges {
|
|
|
100
101
|
/** Everything init writes but `.dek/` and AGENTS.md, in the order a reader meets it. */
|
|
101
102
|
function projectPlan(root: string, deck: string | undefined): PlannedPath[] {
|
|
102
103
|
const themePath = join(root, "theme.css");
|
|
103
|
-
// A first deck is drawn in the theme that will be there: the one kept, or dek's.
|
|
104
|
-
|
|
104
|
+
// A first deck is drawn in the theme that will be there: the one kept, or dek's. A folder that
|
|
105
|
+
// is not there yet holds no theme.
|
|
106
|
+
const theme =
|
|
107
|
+
(existsSync(root) ? readSourceIfExists(themePath, root) : undefined) ?? defaultTheme();
|
|
105
108
|
return [
|
|
106
109
|
{ path: join(root, "dek.toml"), contents: defaultToml() },
|
|
107
110
|
{ path: themePath, contents: defaultTheme() },
|
|
@@ -123,6 +126,8 @@ function checkTarget(root: string): void {
|
|
|
123
126
|
if (existsSync(root) && !statSync(root).isDirectory()) {
|
|
124
127
|
throw new DekError("not a directory", { path: root, hint: "pass a directory to init" });
|
|
125
128
|
}
|
|
129
|
+
// A ref is someone else's project, read where it is: init would write its files into it.
|
|
130
|
+
refuseInsideRef(root);
|
|
126
131
|
const outer = walkUp(root, (dir) => {
|
|
127
132
|
const config = join(dir, "dek.toml");
|
|
128
133
|
return existsSync(config) && statSync(config).isFile() ? dir : undefined;
|
package/src/cli/lint.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { relative } from "node:path";
|
|
3
3
|
import type { Diagnostic } from "../core/diagnostic.ts";
|
|
4
|
+
import { shellQuote } from "../core/escape.ts";
|
|
4
5
|
import { lintDeck, lintProject, unreadableScriptDiagnostics } from "../core/lint.ts";
|
|
5
6
|
import { playwrightMissingError } from "../core/playwright.ts";
|
|
6
|
-
import { resolveRumdlBin, runRumdl } from "../core/rumdl.ts";
|
|
7
|
+
import { type RumdlRunner, type RumdlSkip, resolveRumdlBin, runRumdl } from "../core/rumdl.ts";
|
|
7
8
|
import { mergeSarif, type SarifLog } from "../core/sarif.ts";
|
|
8
9
|
import { syncDeck } from "../core/sync.ts";
|
|
9
10
|
import { lintVisualDeck } from "../core/visual.ts";
|
|
10
|
-
import { type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
|
|
11
|
+
import { RENDERED_RULES, type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
|
|
11
12
|
import type { DecksTarget } from "./scope.ts";
|
|
12
13
|
|
|
13
14
|
export type LintCliResult = {
|
|
@@ -17,10 +18,10 @@ export type LintCliResult = {
|
|
|
17
18
|
rumdlSarif?: SarifLog;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
/** `cwd` is where a skipped rumdl's hint says to run it from. */
|
|
21
|
+
/** `cwd` is where a skipped rumdl's hint says to run it from; `rumdl` runs it, dek's by default. */
|
|
21
22
|
export async function lintCommand(
|
|
22
23
|
{ project, decks, failed = [] }: DecksTarget,
|
|
23
|
-
options: { cwd: string; fix?: boolean; visual?: boolean },
|
|
24
|
+
options: { cwd: string; fix?: boolean; visual?: boolean; rumdl?: RumdlRunner },
|
|
24
25
|
): Promise<LintCliResult> {
|
|
25
26
|
// dek.toml's findings once, then each deck's.
|
|
26
27
|
const diagnostics: Diagnostic[] = [
|
|
@@ -29,19 +30,25 @@ export async function lintCommand(
|
|
|
29
30
|
];
|
|
30
31
|
let rumdlSkip: SkippedCheck | undefined;
|
|
31
32
|
let rumdlSarif: SarifLog | undefined;
|
|
33
|
+
// A rumdl that hung on one deck would hang on each, half a minute apiece.
|
|
34
|
+
let rumdlHung = false;
|
|
32
35
|
|
|
33
36
|
for (const deck of decks) {
|
|
34
37
|
if (options.fix) {
|
|
35
38
|
syncDeck({ project, deck });
|
|
36
39
|
}
|
|
37
40
|
diagnostics.push(...lintDeck({ project, deck }));
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
if (!rumdlHung) {
|
|
42
|
+
const markdown = await runRumdl(deck.scriptPath, options.rumdl);
|
|
43
|
+
diagnostics.push(...markdown.diagnostics);
|
|
44
|
+
// One entry for the run: the first reason, unless rumdl hung, which covers every deck after.
|
|
45
|
+
if (markdown.skipped && (!rumdlSkip || markdown.skipped.hung)) {
|
|
46
|
+
rumdlSkip = rumdlSkipped(deck.scriptPath, options.cwd, markdown.skipped);
|
|
47
|
+
}
|
|
48
|
+
rumdlHung = markdown.skipped?.hung === true;
|
|
49
|
+
if (markdown.sarif) {
|
|
50
|
+
rumdlSarif = rumdlSarif ? mergeSarif(rumdlSarif, markdown.sarif) : markdown.sarif;
|
|
51
|
+
}
|
|
45
52
|
}
|
|
46
53
|
if (options.visual) {
|
|
47
54
|
const visual = await lintVisualDeck({ project, deck });
|
|
@@ -60,7 +67,7 @@ export async function lintCommand(
|
|
|
60
67
|
? []
|
|
61
68
|
: [
|
|
62
69
|
visualSkipped("dekc lint --visual", {
|
|
63
|
-
reason:
|
|
70
|
+
reason: `${RENDERED_RULES} are measured only with --visual`,
|
|
64
71
|
}),
|
|
65
72
|
]),
|
|
66
73
|
]),
|
|
@@ -70,15 +77,28 @@ export async function lintCommand(
|
|
|
70
77
|
|
|
71
78
|
const RUMDL_INSTALL = "bun add -d rumdl; or put rumdl on PATH, or set DEK_RUMDL to its path";
|
|
72
79
|
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
const RUMDL_HUNG =
|
|
81
|
+
"update or reinstall rumdl (`bun add -d rumdl@latest`), or set DEK_RUMDL to a rumdl that finishes; lint skips it for every deck after one it hangs on";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Why rumdl gave no result: whether dek found it, on PATH, in node_modules/.bin, or at DEK_RUMDL,
|
|
85
|
+
* and the reason when dek knows it. One that hung would hang again, so its hint is not to rerun.
|
|
86
|
+
*/
|
|
87
|
+
function rumdlSkipped(scriptPath: string, cwd: string, skip: RumdlSkip): SkippedCheck {
|
|
75
88
|
const bin = resolveRumdlBin();
|
|
76
89
|
if (bin === undefined || !existsSync(bin)) {
|
|
77
90
|
return { check: "rumdl", reason: "rumdl is not installed", hint: RUMDL_INSTALL };
|
|
78
91
|
}
|
|
92
|
+
if (skip.hung) {
|
|
93
|
+
return {
|
|
94
|
+
check: "rumdl",
|
|
95
|
+
reason: skip.reason ?? "rumdl hung and was stopped",
|
|
96
|
+
hint: RUMDL_HUNG,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
79
99
|
return {
|
|
80
100
|
check: "rumdl",
|
|
81
|
-
reason: "rumdl ran but gave no result",
|
|
82
|
-
hint: `run \`${bin} check ${relative(cwd, scriptPath) || scriptPath}\` to see why`,
|
|
101
|
+
reason: skip.reason ?? "rumdl ran but gave no result",
|
|
102
|
+
hint: `run \`${shellQuote(bin)} check ${shellQuote(relative(cwd, scriptPath) || scriptPath)}\` to see why`,
|
|
83
103
|
};
|
|
84
104
|
}
|