@hajimism/dek 0.1.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/LICENSE +21 -0
- package/README.ja.md +75 -0
- package/README.md +75 -0
- package/package.json +65 -0
- package/src/cli/address.ts +53 -0
- package/src/cli/build.ts +85 -0
- package/src/cli/check.ts +97 -0
- package/src/cli/commands.ts +851 -0
- package/src/cli/contract.ts +444 -0
- package/src/cli/cues.ts +18 -0
- package/src/cli/files.ts +309 -0
- package/src/cli/flags.ts +295 -0
- package/src/cli/format.ts +156 -0
- package/src/cli/goto.ts +96 -0
- package/src/cli/init.ts +136 -0
- package/src/cli/keep-alive.ts +72 -0
- package/src/cli/lint.ts +84 -0
- package/src/cli/ls.ts +119 -0
- package/src/cli/main.ts +231 -0
- package/src/cli/marks.ts +16 -0
- package/src/cli/mv.ts +37 -0
- package/src/cli/new.ts +64 -0
- package/src/cli/pdf.ts +16 -0
- package/src/cli/qr.ts +466 -0
- package/src/cli/ref.ts +165 -0
- package/src/cli/rehearse.ts +42 -0
- package/src/cli/result.ts +161 -0
- package/src/cli/scope.ts +308 -0
- package/src/cli/serve.ts +125 -0
- package/src/cli/shot.ts +134 -0
- package/src/cli/show.ts +103 -0
- package/src/cli/sync.ts +37 -0
- package/src/cli/text.ts +479 -0
- package/src/cli/theme.ts +56 -0
- package/src/cli/tty.ts +81 -0
- package/src/cli/usage.ts +198 -0
- package/src/cli/video.ts +17 -0
- package/src/cli/voice.ts +113 -0
- package/src/cli.ts +4 -0
- package/src/core/assets.ts +239 -0
- package/src/core/build.ts +77 -0
- package/src/core/bundled-theme.ts +15 -0
- package/src/core/capture-go.ts +72 -0
- package/src/core/chrome.ts +128 -0
- package/src/core/config-keys.ts +82 -0
- package/src/core/config.ts +95 -0
- package/src/core/css-scan.ts +158 -0
- package/src/core/css-transform.ts +140 -0
- package/src/core/css-value.ts +127 -0
- package/src/core/css.ts +545 -0
- package/src/core/cue.ts +187 -0
- package/src/core/deck-paths.ts +34 -0
- package/src/core/diagnostic.ts +114 -0
- package/src/core/document.ts +218 -0
- package/src/core/error.ts +31 -0
- package/src/core/escape.ts +16 -0
- package/src/core/fill.ts +100 -0
- package/src/core/finish-beat.ts +30 -0
- package/src/core/github.ts +234 -0
- package/src/core/html-scan.ts +253 -0
- package/src/core/html.ts +234 -0
- package/src/core/in-page-go.ts +128 -0
- package/src/core/lines.ts +43 -0
- package/src/core/lint/asset-refs.ts +109 -0
- package/src/core/lint/context.ts +116 -0
- package/src/core/lint/files.ts +117 -0
- package/src/core/lint/keys.ts +48 -0
- package/src/core/lint/morph.ts +91 -0
- package/src/core/lint/script.ts +102 -0
- package/src/core/lint/shared-styles.ts +165 -0
- package/src/core/lint/slide-html.ts +466 -0
- package/src/core/lint/slide-script.ts +179 -0
- package/src/core/lint/theme-css.ts +295 -0
- package/src/core/lint/timing.ts +66 -0
- package/src/core/lint/tokens.ts +520 -0
- package/src/core/lint/voice.ts +76 -0
- package/src/core/lint.ts +137 -0
- package/src/core/live-protocol.ts +130 -0
- package/src/core/marks.ts +198 -0
- package/src/core/mv.ts +293 -0
- package/src/core/ogp.ts +99 -0
- package/src/core/optional.ts +24 -0
- package/src/core/overflow.ts +220 -0
- package/src/core/page-pool.ts +53 -0
- package/src/core/page.ts +66 -0
- package/src/core/parse.ts +307 -0
- package/src/core/path.ts +58 -0
- package/src/core/pdf.ts +71 -0
- package/src/core/playwright-module.d.ts +43 -0
- package/src/core/playwright-motion.ts +42 -0
- package/src/core/playwright-sheet.ts +32 -0
- package/src/core/playwright-visual.ts +391 -0
- package/src/core/playwright-worker.ts +5 -0
- package/src/core/playwright.ts +431 -0
- package/src/core/position.ts +146 -0
- package/src/core/presenter-state.ts +43 -0
- package/src/core/presenter.ts +131 -0
- package/src/core/prose.ts +16 -0
- package/src/core/pseudo-text.ts +67 -0
- package/src/core/rail-width.ts +4 -0
- package/src/core/ref-name.ts +110 -0
- package/src/core/ref.ts +238 -0
- package/src/core/resolve.ts +228 -0
- package/src/core/rumdl.ts +99 -0
- package/src/core/safe-fs.ts +196 -0
- package/src/core/sarif.ts +135 -0
- package/src/core/schema.ts +89 -0
- package/src/core/script-lines.ts +53 -0
- package/src/core/sheet.ts +223 -0
- package/src/core/shot/morph.ts +80 -0
- package/src/core/shot/motion.ts +187 -0
- package/src/core/shot/still.ts +216 -0
- package/src/core/shot-cache.ts +140 -0
- package/src/core/size.ts +11 -0
- package/src/core/skeleton-record.ts +51 -0
- package/src/core/skeleton.ts +89 -0
- package/src/core/slide-draw.ts +165 -0
- package/src/core/slide-eval-worker.ts +58 -0
- package/src/core/slide-measure.ts +272 -0
- package/src/core/slide-script-eval.ts +258 -0
- package/src/core/slide-script.ts +252 -0
- package/src/core/spawn.ts +128 -0
- package/src/core/step.ts +94 -0
- package/src/core/still-page.ts +70 -0
- package/src/core/suggest.ts +33 -0
- package/src/core/sync.ts +316 -0
- package/src/core/text-contrast.ts +466 -0
- package/src/core/theme-excerpt.ts +187 -0
- package/src/core/theme-facts.ts +51 -0
- package/src/core/timeline.ts +216 -0
- package/src/core/timing.ts +88 -0
- package/src/core/toml-keys.ts +349 -0
- package/src/core/url-attributes.ts +103 -0
- package/src/core/video-document.ts +15 -0
- package/src/core/visual-cache.ts +100 -0
- package/src/core/visual.ts +558 -0
- package/src/core/voice.ts +321 -0
- package/src/core/zod.ts +79 -0
- package/src/runtime/browser.ts +203 -0
- package/src/runtime/clone.ts +52 -0
- package/src/runtime/deck-control.ts +43 -0
- package/src/runtime/fit.ts +32 -0
- package/src/runtime/go.ts +56 -0
- package/src/runtime/input.ts +128 -0
- package/src/runtime/laser-view.ts +110 -0
- package/src/runtime/laser.ts +145 -0
- package/src/runtime/live-host.ts +33 -0
- package/src/runtime/live-reload.ts +29 -0
- package/src/runtime/live.ts +88 -0
- package/src/runtime/marks-view.ts +74 -0
- package/src/runtime/motion.ts +113 -0
- package/src/runtime/navigator.ts +189 -0
- package/src/runtime/player.ts +28 -0
- package/src/runtime/presenter-view.ts +289 -0
- package/src/runtime/presenter.ts +52 -0
- package/src/runtime/rail-view.ts +229 -0
- package/src/runtime/rail.ts +52 -0
- package/src/runtime/rehearse.ts +264 -0
- package/src/runtime/routes.ts +142 -0
- package/src/runtime/settle.ts +45 -0
- package/src/runtime/slide.d.ts +28 -0
- package/src/runtime/socket.ts +143 -0
- package/src/runtime/stage.ts +259 -0
- package/src/runtime/step.ts +139 -0
- package/src/runtime/window.ts +38 -0
- package/src/server/auth.ts +210 -0
- package/src/server/deck-snapshot.ts +146 -0
- package/src/server/dev.ts +251 -0
- package/src/server/hub.ts +104 -0
- package/src/server/lan.ts +51 -0
- package/src/server/lock.ts +97 -0
- package/src/server/pages.ts +71 -0
- package/src/server/pairing.ts +40 -0
- package/src/server/project-watch.ts +142 -0
- package/src/server/rooms.ts +133 -0
- package/src/server/routes.ts +286 -0
- package/src/server/serial.ts +25 -0
- package/src/server/watch.ts +308 -0
- package/src/theme/default.css +175 -0
- package/src/video/bake.ts +152 -0
- package/src/video/mux.ts +103 -0
- package/src/video/recorder.ts +115 -0
- package/src/video/sidecar.ts +60 -0
- package/src/video/worker.ts +51 -0
- package/src/voice/engine.ts +177 -0
- package/src/voice/synth.ts +127 -0
- package/src/voice/wav.ts +115 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { isAbsolute, relative } from "node:path";
|
|
2
|
+
import type { Diagnostic } from "../core/diagnostic.ts";
|
|
3
|
+
import { type ErrorFields, errorFields } from "../core/error.ts";
|
|
4
|
+
import type { LiveEvent } from "../core/live-protocol.ts";
|
|
5
|
+
import { ansi, displayWidth, padEndWidth, padStartWidth } from "./tty.ts";
|
|
6
|
+
|
|
7
|
+
function formatLocation(location: { path?: string; line?: number; column?: number }): string {
|
|
8
|
+
if (location.path !== undefined && location.line !== undefined) {
|
|
9
|
+
const column = location.column === undefined ? "" : `:${location.column}`;
|
|
10
|
+
return `${location.path}:${location.line}${column}`;
|
|
11
|
+
}
|
|
12
|
+
if (location.path !== undefined) {
|
|
13
|
+
return location.path;
|
|
14
|
+
}
|
|
15
|
+
if (location.line !== undefined) {
|
|
16
|
+
return String(location.line);
|
|
17
|
+
}
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatDiagnostics(
|
|
22
|
+
diagnostics: Diagnostic[],
|
|
23
|
+
opts?: { color?: boolean; cwd?: string },
|
|
24
|
+
): string {
|
|
25
|
+
if (diagnostics.length === 0) {
|
|
26
|
+
return "no diagnostics";
|
|
27
|
+
}
|
|
28
|
+
const c = ansi(opts?.color === true);
|
|
29
|
+
const lines = diagnostics.flatMap((diagnostic) => {
|
|
30
|
+
const where = formatLocation({
|
|
31
|
+
...diagnostic,
|
|
32
|
+
...(diagnostic.path !== undefined ? { path: displayPath(diagnostic.path, opts?.cwd) } : {}),
|
|
33
|
+
});
|
|
34
|
+
const prefix = where ? `${c.cyan(where)}: ` : "";
|
|
35
|
+
const label = diagnostic.severity === "warning" ? ` ${c.yellow("warning:")}` : "";
|
|
36
|
+
const line = `${prefix}${c.yellow(diagnostic.id)}${label} ${diagnostic.message}`;
|
|
37
|
+
return diagnostic.hint ? [line, ` ${c.yellow("help:")} ${diagnostic.hint}`] : [line];
|
|
38
|
+
});
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type FormattedError = ErrorFields;
|
|
43
|
+
|
|
44
|
+
/** A path as the reader should type it: relative to `cwd` when one is given. */
|
|
45
|
+
export function displayPath(path: string, cwd?: string): string {
|
|
46
|
+
if (cwd === undefined || !isAbsolute(path)) {
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
return relative(cwd, path) || ".";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatError(error: unknown, opts?: { cwd?: string }): FormattedError {
|
|
53
|
+
const fields = errorFields(error);
|
|
54
|
+
return fields.path === undefined
|
|
55
|
+
? fields
|
|
56
|
+
: { ...fields, path: displayPath(fields.path, opts?.cwd) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatErrorText(error: unknown, opts?: { color?: boolean; cwd?: string }): string {
|
|
60
|
+
const formatted = formatError(error, opts);
|
|
61
|
+
const c = ansi(opts?.color === true);
|
|
62
|
+
const parts = [`${c.bold(c.red("error:"))} ${formatted.message}`];
|
|
63
|
+
if (formatted.path) {
|
|
64
|
+
const loc =
|
|
65
|
+
formatted.line !== undefined ? `${formatted.path}:${formatted.line}` : formatted.path;
|
|
66
|
+
parts.push(` ${c.cyan("-->")} ${loc}`);
|
|
67
|
+
}
|
|
68
|
+
if (formatted.hint) {
|
|
69
|
+
parts.push(` ${c.yellow("help:")} ${formatted.hint}`);
|
|
70
|
+
}
|
|
71
|
+
return parts.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatTable(
|
|
75
|
+
headers: string[],
|
|
76
|
+
rows: string[][],
|
|
77
|
+
align?: Array<"left" | "right">,
|
|
78
|
+
): string {
|
|
79
|
+
const widths = headers.map((header, index) =>
|
|
80
|
+
Math.max(displayWidth(header), ...rows.map((row) => displayWidth(row[index] ?? ""))),
|
|
81
|
+
);
|
|
82
|
+
const pad = (cell: string, index: number): string => {
|
|
83
|
+
const width = widths[index] ?? 0;
|
|
84
|
+
return (align?.[index] ?? "left") === "right"
|
|
85
|
+
? padStartWidth(cell, width)
|
|
86
|
+
: padEndWidth(cell, width);
|
|
87
|
+
};
|
|
88
|
+
const line = (cells: string[]): string => cells.map((cell, index) => pad(cell, index)).join(" ");
|
|
89
|
+
return [line(headers), ...rows.map((row) => line(row))].join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatCreated(
|
|
93
|
+
created: string[],
|
|
94
|
+
updated: string[] = [],
|
|
95
|
+
removed: string[] = [],
|
|
96
|
+
): string {
|
|
97
|
+
const count = created.length + updated.length + removed.length;
|
|
98
|
+
const header = `synced ${count} ${count === 1 ? "file" : "files"}`;
|
|
99
|
+
const lines = [
|
|
100
|
+
...created,
|
|
101
|
+
...updated.map((path) => `${path} (updated)`),
|
|
102
|
+
...removed.map((path) => `${path} (removed)`),
|
|
103
|
+
];
|
|
104
|
+
return [header, ...lines.map((line) => ` ${line}`)].join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A dev server event as the terminal shows it. `deck` names the deck on each line that starts an
|
|
109
|
+
* entry, for a server that serves several; lines that continue one stay indented under it.
|
|
110
|
+
*/
|
|
111
|
+
export function formatDevEvent(
|
|
112
|
+
event: LiveEvent,
|
|
113
|
+
opts?: { cwd?: string; deck?: string },
|
|
114
|
+
): string | null {
|
|
115
|
+
const text = formatLiveEvent(event, opts?.cwd);
|
|
116
|
+
const deck = opts?.deck;
|
|
117
|
+
if (text === null || deck === undefined) {
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
return text
|
|
121
|
+
.split("\n")
|
|
122
|
+
.map((line) => (/^\s/.test(line) ? line : `[${deck}] ${line}`))
|
|
123
|
+
.join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatLiveEvent(event: LiveEvent, cwd: string | undefined): string | null {
|
|
127
|
+
switch (event.type) {
|
|
128
|
+
case "reload-slide":
|
|
129
|
+
return `reload-slide ${event.slug}`;
|
|
130
|
+
case "reload-theme":
|
|
131
|
+
return "reload-theme";
|
|
132
|
+
case "reload-script":
|
|
133
|
+
return `reload-script ${event.slugs.join(", ")}`;
|
|
134
|
+
case "sync":
|
|
135
|
+
if ((event.removed?.length ?? 0) > 0 && event.created.length === 0) {
|
|
136
|
+
const count = event.removed?.length ?? 0;
|
|
137
|
+
return `removed ${count} ${count === 1 ? "file" : "files"}`;
|
|
138
|
+
}
|
|
139
|
+
return formatCreated(event.created, event.updated, event.removed);
|
|
140
|
+
case "diagnostics":
|
|
141
|
+
return event.diagnostics.length === 0 ? null : formatDiagnostics(event.diagnostics, { cwd });
|
|
142
|
+
case "timeline":
|
|
143
|
+
return "timeline";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function writeDevEvent(
|
|
148
|
+
event: LiveEvent,
|
|
149
|
+
stream: { write(chunk: string): unknown },
|
|
150
|
+
opts?: { cwd?: string; deck?: string },
|
|
151
|
+
): void {
|
|
152
|
+
const text = formatDevEvent(event, opts);
|
|
153
|
+
if (text) {
|
|
154
|
+
stream.write(`${text}\n`);
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/cli/goto.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { DekError } from "../core/error.ts";
|
|
2
|
+
import { deckRoutePath } from "../runtime/routes.ts";
|
|
3
|
+
import { isPidAlive, readDevServerLock } from "../server/lock.ts";
|
|
4
|
+
import { type DeckTarget, requireSection } from "./scope.ts";
|
|
5
|
+
|
|
6
|
+
export type NavResult = {
|
|
7
|
+
slug: string;
|
|
8
|
+
slideIndex: number;
|
|
9
|
+
beatIndex: number;
|
|
10
|
+
/** The pages that show the deck; with none, the position is where the next one lands. */
|
|
11
|
+
viewers: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function gotoCommand(target: DeckTarget, slug: string): Promise<NavResult> {
|
|
15
|
+
requireSection(target.deck, slug);
|
|
16
|
+
return requestDevServer(target, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
path: "goto",
|
|
19
|
+
body: { slug },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function currentCommand(target: DeckTarget): Promise<NavResult> {
|
|
24
|
+
return requestDevServer(target, { method: "GET", path: "current" });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function requestDevServer(
|
|
28
|
+
{ project, deck }: DeckTarget,
|
|
29
|
+
request: { method: "GET" | "POST"; path: "goto" | "current"; body?: { slug: string } },
|
|
30
|
+
): Promise<NavResult> {
|
|
31
|
+
const deckName = deck.name;
|
|
32
|
+
const lock = readDevServerLock(project.root);
|
|
33
|
+
if (!lock || !isPidAlive(lock.pid)) {
|
|
34
|
+
throw new DekError("dev server is not running", { hint: "run `dekc`" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Under /decks/<name> whether or not the server is scoped to this deck; a scoped one takes both.
|
|
38
|
+
const url = new URL(deckRoutePath(deckName, { kind: request.path }), lock.url);
|
|
39
|
+
const headers: Record<string, string> = {};
|
|
40
|
+
if (request.body) {
|
|
41
|
+
headers["content-type"] = "application/json";
|
|
42
|
+
}
|
|
43
|
+
if (lock.password) {
|
|
44
|
+
headers.authorization = `Basic ${btoa(`:${lock.password}`)}`;
|
|
45
|
+
}
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
method: request.method,
|
|
48
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
49
|
+
body: request.body ? JSON.stringify(request.body) : undefined,
|
|
50
|
+
});
|
|
51
|
+
// Only the nav routes answer JSON; a 404, 401, or 405 from the server itself is plain text.
|
|
52
|
+
if (!response.headers.get("content-type")?.includes("application/json")) {
|
|
53
|
+
throw refusal(response.status, lock, deckName, request.path);
|
|
54
|
+
}
|
|
55
|
+
const json = (await response.json()) as {
|
|
56
|
+
ok?: boolean;
|
|
57
|
+
slug?: string;
|
|
58
|
+
slideIndex?: number;
|
|
59
|
+
beatIndex?: number;
|
|
60
|
+
viewers?: number;
|
|
61
|
+
error?: { message?: string };
|
|
62
|
+
};
|
|
63
|
+
if (!response.ok || json.ok === false || json.slug === undefined) {
|
|
64
|
+
throw new DekError(json.error?.message ?? `dev server ${request.path} failed`, {
|
|
65
|
+
hint: "run `dekc`",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
slug: json.slug,
|
|
70
|
+
slideIndex: json.slideIndex ?? 0,
|
|
71
|
+
beatIndex: json.beatIndex ?? 0,
|
|
72
|
+
viewers: json.viewers ?? 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Why the server turned the request away before a nav route saw it. */
|
|
77
|
+
function refusal(
|
|
78
|
+
status: number,
|
|
79
|
+
lock: { url: string; deck?: string },
|
|
80
|
+
deckName: string,
|
|
81
|
+
path: string,
|
|
82
|
+
): DekError {
|
|
83
|
+
if (status === 404 && lock.deck !== undefined && lock.deck !== deckName) {
|
|
84
|
+
return new DekError(`the running dev server serves deck "${lock.deck}", not "${deckName}"`, {
|
|
85
|
+
hint: `pass --deck ${lock.deck}, or stop the server at ${lock.url} and run \`dekc ${deckName}\``,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (status === 401) {
|
|
89
|
+
return new DekError(`the dev server at ${lock.url} refused the password in .dek/server.json`, {
|
|
90
|
+
hint: "restart it: stop `dekc` and run it again",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return new DekError(`dev server ${path} failed with HTTP ${status}`, {
|
|
94
|
+
hint: `restart the server at ${lock.url}: stop \`dek\` and run it again`,
|
|
95
|
+
});
|
|
96
|
+
}
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { parseCss, publishedTokens } from "../core/css.ts";
|
|
4
|
+
import { deckPaths } from "../core/deck-paths.ts";
|
|
5
|
+
import { DekError } from "../core/error.ts";
|
|
6
|
+
import { REQUIRED_TOKENS } from "../core/lint/tokens.ts";
|
|
7
|
+
import { walkUp } from "../core/optional.ts";
|
|
8
|
+
import { DECK_NAME_HINT, isDeckName } from "../core/path.ts";
|
|
9
|
+
import { readTextIfExists } from "../core/resolve.ts";
|
|
10
|
+
import {
|
|
11
|
+
defaultTsconfig,
|
|
12
|
+
dekFilePaths,
|
|
13
|
+
type FileChanges,
|
|
14
|
+
syncDeck,
|
|
15
|
+
writeDekFiles,
|
|
16
|
+
} from "../core/sync.ts";
|
|
17
|
+
import {
|
|
18
|
+
applyPlan,
|
|
19
|
+
checkFileSlots,
|
|
20
|
+
deckPlan,
|
|
21
|
+
defaultGitignore,
|
|
22
|
+
defaultRumdl,
|
|
23
|
+
defaultTheme,
|
|
24
|
+
defaultToml,
|
|
25
|
+
nextSteps,
|
|
26
|
+
type PlannedPath,
|
|
27
|
+
playwrightStep,
|
|
28
|
+
shellQuote,
|
|
29
|
+
} from "./files.ts";
|
|
30
|
+
|
|
31
|
+
export type InitResult = {
|
|
32
|
+
root: string;
|
|
33
|
+
/** Paths init wrote. */
|
|
34
|
+
created: string[];
|
|
35
|
+
/** dek's own files, already there, that init brought up to date. */
|
|
36
|
+
updated: string[];
|
|
37
|
+
/** Files that were already there and differ from what init would write; left as they are. */
|
|
38
|
+
kept: string[];
|
|
39
|
+
/** The commands to run next, from the directory init ran in. */
|
|
40
|
+
next: string[];
|
|
41
|
+
/** The Playwright install, when the project lacks it: `dekc shot`, `dekc pdf`, and `--visual` need it. */
|
|
42
|
+
playwright?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The tokens a kept theme.css lacks of the ones every deck needs, when it lacks any. Each deck
|
|
45
|
+
* copies the project theme, so its lint would report them all on its first run.
|
|
46
|
+
*/
|
|
47
|
+
missingTokens?: string[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Plans the whole project, checks it, then writes only what is missing. Run
|
|
52
|
+
* again, it fills in what is gone and keeps everything else, edits included.
|
|
53
|
+
*/
|
|
54
|
+
export function initCommand(options: { cwd: string; dir?: string; deck?: string }): InitResult {
|
|
55
|
+
const root = resolve(options.cwd, options.dir ?? ".");
|
|
56
|
+
if (options.deck !== undefined && !isDeckName(options.deck)) {
|
|
57
|
+
throw new DekError(`invalid deck name "${options.deck}"`, {
|
|
58
|
+
hint: DECK_NAME_HINT,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
checkTarget(root);
|
|
62
|
+
checkFileSlots(dekFilePaths(root));
|
|
63
|
+
|
|
64
|
+
const { created, kept } = applyPlan(projectPlan(root, options.deck));
|
|
65
|
+
const deckDir = options.deck ? join(root, "decks", options.deck) : undefined;
|
|
66
|
+
// A starter script init just wrote gets its skeletons as sync writes them; a script that was
|
|
67
|
+
// already there is the author's, and so is whether it parses.
|
|
68
|
+
const started = deckDir !== undefined && created.includes(deckPaths(deckDir).script);
|
|
69
|
+
const dekFiles = started ? syncDeckFiles(deckDir, created) : writeDekFiles(root);
|
|
70
|
+
created.push(...dekFiles.created);
|
|
71
|
+
const playwright = playwrightStep(root);
|
|
72
|
+
const missingTokens = kept.includes(join(root, "theme.css")) ? missingThemeTokens(root) : [];
|
|
73
|
+
return {
|
|
74
|
+
root,
|
|
75
|
+
created,
|
|
76
|
+
updated: dekFiles.updated,
|
|
77
|
+
kept,
|
|
78
|
+
next: nextSteps(options.cwd, root, deckDir),
|
|
79
|
+
...(playwright ? { playwright } : {}),
|
|
80
|
+
...(missingTokens.length > 0 ? { missingTokens } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The tokens every deck needs that the project's theme.css does not publish on `.slide`. */
|
|
85
|
+
function missingThemeTokens(root: string): string[] {
|
|
86
|
+
const published = new Set(
|
|
87
|
+
publishedTokens(parseCss(readTextIfExists(join(root, "theme.css")) ?? "")).map(
|
|
88
|
+
(token) => token.name,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
return REQUIRED_TOKENS.filter((name) => !published.has(name));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function syncDeckFiles(deckDir: string, created: string[]): FileChanges {
|
|
95
|
+
const synced = syncDeck(deckDir);
|
|
96
|
+
created.push(...synced.created);
|
|
97
|
+
return synced.dekFiles;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Everything init writes but `.dek/` and AGENTS.md, in the order a reader meets it. */
|
|
101
|
+
function projectPlan(root: string, deck: string | undefined): PlannedPath[] {
|
|
102
|
+
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
|
+
const theme = readTextIfExists(themePath) ?? defaultTheme();
|
|
105
|
+
return [
|
|
106
|
+
{ path: join(root, "dek.toml"), contents: defaultToml() },
|
|
107
|
+
{ path: themePath, contents: defaultTheme() },
|
|
108
|
+
{ path: join(root, ".rumdl.toml"), contents: defaultRumdl() },
|
|
109
|
+
{ path: join(root, ".gitignore"), contents: defaultGitignore() },
|
|
110
|
+
{ path: join(root, "tsconfig.json"), contents: defaultTsconfig() },
|
|
111
|
+
{ path: join(root, "assets") },
|
|
112
|
+
{ path: join(root, "decks") },
|
|
113
|
+
...(deck ? deckPlan(root, deck, theme) : []),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The target is a directory, or nothing yet, and no project already holds it:
|
|
119
|
+
* a project inside another would be a second root the outer one cannot see,
|
|
120
|
+
* and its decks belong under the outer project's decks/ instead.
|
|
121
|
+
*/
|
|
122
|
+
function checkTarget(root: string): void {
|
|
123
|
+
if (existsSync(root) && !statSync(root).isDirectory()) {
|
|
124
|
+
throw new DekError("not a directory", { path: root, hint: "pass a directory to init" });
|
|
125
|
+
}
|
|
126
|
+
const outer = walkUp(root, (dir) => {
|
|
127
|
+
const config = join(dir, "dek.toml");
|
|
128
|
+
return existsSync(config) && statSync(config).isFile() ? dir : undefined;
|
|
129
|
+
});
|
|
130
|
+
if (outer !== undefined && outer !== root) {
|
|
131
|
+
throw new DekError(`already inside the dek project at ${outer}`, {
|
|
132
|
+
path: join(outer, "dek.toml"),
|
|
133
|
+
hint: `add a deck to that project with \`dekc new ${shellQuote(basename(root))}\`, or run init outside it`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { DevServer } from "../server/dev.ts";
|
|
2
|
+
import { writeDevEvent } from "./format.ts";
|
|
3
|
+
|
|
4
|
+
/** How often the server checks that whoever started it is still there. */
|
|
5
|
+
const PARENT_POLL_MS = 1_000;
|
|
6
|
+
|
|
7
|
+
/** How long a stop waits for the server to close before the process exits anyway. */
|
|
8
|
+
const SHUTDOWN_GRACE_MS = 2_000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Serve until told to stop: Ctrl-C, a kill, a closed terminal, or the process that started the
|
|
12
|
+
* server going away, so a test run or an editor that dies without cleaning up leaves no server
|
|
13
|
+
* holding a port.
|
|
14
|
+
*/
|
|
15
|
+
export async function keepDevServer(server: DevServer): Promise<void> {
|
|
16
|
+
const stop = stopOnce(
|
|
17
|
+
() => server.close(),
|
|
18
|
+
(code) => process.exit(code),
|
|
19
|
+
);
|
|
20
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
|
|
21
|
+
process.on(signal, stop);
|
|
22
|
+
}
|
|
23
|
+
onOrphaned(stop);
|
|
24
|
+
// A server scoped to one deck has nothing to tell apart; one that serves several names each.
|
|
25
|
+
server.events.listen((event) =>
|
|
26
|
+
writeDevEvent(event, process.stderr, {
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
...(server.deckDir === undefined ? { deck: event.deck } : {}),
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
await new Promise<void>(() => {
|
|
32
|
+
/* keep the process alive until stopped */
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A stop that closes once however often it is asked, and exits even if the close never ends. */
|
|
37
|
+
export function stopOnce(
|
|
38
|
+
close: () => Promise<void>,
|
|
39
|
+
exit: (code: number) => void,
|
|
40
|
+
graceMs = SHUTDOWN_GRACE_MS,
|
|
41
|
+
): () => void {
|
|
42
|
+
let stopping = false;
|
|
43
|
+
return () => {
|
|
44
|
+
if (stopping) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
stopping = true;
|
|
48
|
+
setTimeout(() => exit(0), graceMs).unref();
|
|
49
|
+
void close().finally(() => exit(0));
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Call `fn` once this process is reparented, which is how Unix says the parent died: a SIGKILL
|
|
55
|
+
* leaves it no chance to stop its children. A process started with no parent of its own to lose
|
|
56
|
+
* (already under init) is never orphaned.
|
|
57
|
+
*/
|
|
58
|
+
export function onOrphaned(
|
|
59
|
+
fn: () => void,
|
|
60
|
+
options: { ppid?: () => number; intervalMs?: number } = {},
|
|
61
|
+
): () => void {
|
|
62
|
+
const ppid = options.ppid ?? (() => process.ppid);
|
|
63
|
+
const parent = ppid();
|
|
64
|
+
const timer = setInterval(() => {
|
|
65
|
+
if (ppid() !== parent) {
|
|
66
|
+
clearInterval(timer);
|
|
67
|
+
fn();
|
|
68
|
+
}
|
|
69
|
+
}, options.intervalMs ?? PARENT_POLL_MS);
|
|
70
|
+
timer.unref();
|
|
71
|
+
return () => clearInterval(timer);
|
|
72
|
+
}
|
package/src/cli/lint.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { relative } from "node:path";
|
|
3
|
+
import type { Diagnostic } from "../core/diagnostic.ts";
|
|
4
|
+
import { lintDeck, lintProject, unreadableScriptDiagnostics } from "../core/lint.ts";
|
|
5
|
+
import { playwrightMissingError } from "../core/playwright.ts";
|
|
6
|
+
import { resolveRumdlBin, runRumdl } from "../core/rumdl.ts";
|
|
7
|
+
import { mergeSarif, type SarifLog } from "../core/sarif.ts";
|
|
8
|
+
import { syncDeck } from "../core/sync.ts";
|
|
9
|
+
import { lintVisualDeck } from "../core/visual.ts";
|
|
10
|
+
import { type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
|
|
11
|
+
import type { DecksTarget } from "./scope.ts";
|
|
12
|
+
|
|
13
|
+
export type LintCliResult = {
|
|
14
|
+
diagnostics: Diagnostic[];
|
|
15
|
+
/** Checks that did not run, each with why and how to run it, so an empty list is not a pass. */
|
|
16
|
+
skipped?: SkippedCheck[];
|
|
17
|
+
rumdlSarif?: SarifLog;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** `cwd` is where a skipped rumdl's hint says to run it from. */
|
|
21
|
+
export async function lintCommand(
|
|
22
|
+
{ project, decks, failed = [] }: DecksTarget,
|
|
23
|
+
options: { cwd: string; fix?: boolean; visual?: boolean },
|
|
24
|
+
): Promise<LintCliResult> {
|
|
25
|
+
// dek.toml's findings once, then each deck's.
|
|
26
|
+
const diagnostics: Diagnostic[] = [
|
|
27
|
+
...lintProject(project),
|
|
28
|
+
...failed.flatMap(unreadableScriptDiagnostics),
|
|
29
|
+
];
|
|
30
|
+
let rumdlSkip: SkippedCheck | undefined;
|
|
31
|
+
let rumdlSarif: SarifLog | undefined;
|
|
32
|
+
|
|
33
|
+
for (const deck of decks) {
|
|
34
|
+
if (options.fix) {
|
|
35
|
+
syncDeck({ project, deck });
|
|
36
|
+
}
|
|
37
|
+
diagnostics.push(...lintDeck({ project, deck }));
|
|
38
|
+
const markdown = await runRumdl(deck.scriptPath);
|
|
39
|
+
diagnostics.push(...markdown.diagnostics);
|
|
40
|
+
if (markdown.skipped) {
|
|
41
|
+
rumdlSkip ??= rumdlSkipped(deck.scriptPath, options.cwd);
|
|
42
|
+
}
|
|
43
|
+
if (markdown.sarif) {
|
|
44
|
+
rumdlSarif = rumdlSarif ? mergeSarif(rumdlSarif, markdown.sarif) : markdown.sarif;
|
|
45
|
+
}
|
|
46
|
+
if (options.visual) {
|
|
47
|
+
const visual = await lintVisualDeck({ project, deck });
|
|
48
|
+
if (visual === null) {
|
|
49
|
+
throw playwrightMissingError();
|
|
50
|
+
}
|
|
51
|
+
diagnostics.push(...visual);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
diagnostics,
|
|
57
|
+
...skippedChecks([
|
|
58
|
+
...(rumdlSkip ? [rumdlSkip] : []),
|
|
59
|
+
...(options.visual
|
|
60
|
+
? []
|
|
61
|
+
: [
|
|
62
|
+
visualSkipped("dekc lint --visual", {
|
|
63
|
+
reason: "overflow and contrast are measured only with --visual",
|
|
64
|
+
}),
|
|
65
|
+
]),
|
|
66
|
+
]),
|
|
67
|
+
...(rumdlSarif ? { rumdlSarif } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const RUMDL_INSTALL = "bun add -d rumdl; or put rumdl on PATH, or set DEK_RUMDL to its path";
|
|
72
|
+
|
|
73
|
+
/** Why rumdl gave no result: dek looks for it on PATH, in node_modules/.bin, and at DEK_RUMDL. */
|
|
74
|
+
function rumdlSkipped(scriptPath: string, cwd: string): SkippedCheck {
|
|
75
|
+
const bin = resolveRumdlBin();
|
|
76
|
+
if (bin === undefined || !existsSync(bin)) {
|
|
77
|
+
return { check: "rumdl", reason: "rumdl is not installed", hint: RUMDL_INSTALL };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
check: "rumdl",
|
|
81
|
+
reason: "rumdl ran but gave no result",
|
|
82
|
+
hint: `run \`${bin} check ${relative(cwd, scriptPath) || scriptPath}\` to see why`,
|
|
83
|
+
};
|
|
84
|
+
}
|
package/src/cli/ls.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type DekConfig, loadConfig } from "../core/config.ts";
|
|
2
|
+
import type { Diagnostic } from "../core/diagnostic.ts";
|
|
3
|
+
import { lintDeck, lintProject } from "../core/lint.ts";
|
|
4
|
+
import { listSlides, type Project, type ProjectDeck } from "../core/resolve.ts";
|
|
5
|
+
import { slideVideoSeconds, timelineSeconds } from "../core/timeline.ts";
|
|
6
|
+
import { sectionTiming } from "../core/timing.ts";
|
|
7
|
+
import { tryLoadCachedTimeline } from "../core/voice.ts";
|
|
8
|
+
import { type SkippedCheck, skippedChecks } from "./result.ts";
|
|
9
|
+
import type { DecksTarget, RefInfo } from "./scope.ts";
|
|
10
|
+
|
|
11
|
+
export type LsListResult = {
|
|
12
|
+
kind: "list";
|
|
13
|
+
root: string;
|
|
14
|
+
/** The project's own findings (dek.toml), once: no deck's row repeats them. */
|
|
15
|
+
diagnostics: Diagnostic[];
|
|
16
|
+
decks: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
title: string;
|
|
19
|
+
sections: number;
|
|
20
|
+
slides: number;
|
|
21
|
+
diagnostics: Diagnostic[];
|
|
22
|
+
}>;
|
|
23
|
+
failed: Array<{ name: string }>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type LsSectionRow = {
|
|
27
|
+
slug: string;
|
|
28
|
+
title: string;
|
|
29
|
+
beats: number;
|
|
30
|
+
estimateSeconds: number;
|
|
31
|
+
budgetSeconds?: number;
|
|
32
|
+
videoSeconds?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type LsDeckResult = {
|
|
36
|
+
kind: "deck";
|
|
37
|
+
name: string;
|
|
38
|
+
title: string;
|
|
39
|
+
event?: string;
|
|
40
|
+
date?: string;
|
|
41
|
+
duration?: string;
|
|
42
|
+
estimateSeconds: number;
|
|
43
|
+
videoSeconds?: number;
|
|
44
|
+
sections: LsSectionRow[];
|
|
45
|
+
diagnostics: Diagnostic[];
|
|
46
|
+
/** Lint is skipped for a ref: it is not yours to fix, so an empty list is not a pass. */
|
|
47
|
+
skipped?: SkippedCheck[];
|
|
48
|
+
/** Set when the deck is a ref. */
|
|
49
|
+
ref?: RefInfo;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The one deck in scope, a ref included, or else every deck of the project. */
|
|
53
|
+
export function lsCommand(target: DecksTarget): LsListResult | LsDeckResult {
|
|
54
|
+
const { project, deck, ref } = target;
|
|
55
|
+
if (deck && ref) {
|
|
56
|
+
return {
|
|
57
|
+
...formatDeck(deck, loadConfig(project.configPath), project, { lint: false }),
|
|
58
|
+
...skippedChecks([
|
|
59
|
+
{ check: "lint", reason: "a ref is read-only; its problems are not yours to fix" },
|
|
60
|
+
]),
|
|
61
|
+
ref,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (deck) {
|
|
65
|
+
return formatDeck(deck, loadConfig(project.configPath), project);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
kind: "list",
|
|
69
|
+
root: project.root,
|
|
70
|
+
diagnostics: lintProject(project),
|
|
71
|
+
decks: project.decks.map((entry) => summarizeDeck(entry, project)),
|
|
72
|
+
failed: project.failed.map((entry) => ({ name: entry.name })),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function summarizeDeck(deck: ProjectDeck, project: Project): LsListResult["decks"][number] {
|
|
77
|
+
return {
|
|
78
|
+
name: deck.name,
|
|
79
|
+
title: deck.deck.title,
|
|
80
|
+
sections: deck.deck.sections.length,
|
|
81
|
+
slides: listSlides(deck.dir).length,
|
|
82
|
+
diagnostics: lintDeck({ project, deck }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatDeck(
|
|
87
|
+
deck: ProjectDeck,
|
|
88
|
+
config: DekConfig,
|
|
89
|
+
project: Project,
|
|
90
|
+
options: { lint: boolean } = { lint: true },
|
|
91
|
+
): LsDeckResult {
|
|
92
|
+
const timing = sectionTiming(deck.deck.sections, deck.deck.duration, config);
|
|
93
|
+
const estimateTotal = timing.reduce((sum, row) => sum + row.estimateSeconds, 0);
|
|
94
|
+
const timeline = tryLoadCachedTimeline(deck.dir);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
kind: "deck",
|
|
98
|
+
name: deck.name,
|
|
99
|
+
title: deck.deck.title,
|
|
100
|
+
...(deck.deck.event ? { event: deck.deck.event } : {}),
|
|
101
|
+
...(deck.deck.date ? { date: deck.deck.date } : {}),
|
|
102
|
+
...(deck.deck.duration ? { duration: deck.deck.duration } : {}),
|
|
103
|
+
estimateSeconds: estimateTotal,
|
|
104
|
+
...(timeline ? { videoSeconds: timelineSeconds(timeline) } : {}),
|
|
105
|
+
sections: deck.deck.sections.map((section, index) => {
|
|
106
|
+
const row = timing[index];
|
|
107
|
+
const videoSeconds = timeline ? slideVideoSeconds(timeline, index) : undefined;
|
|
108
|
+
return {
|
|
109
|
+
slug: section.slug,
|
|
110
|
+
title: section.title,
|
|
111
|
+
beats: section.beats.length,
|
|
112
|
+
estimateSeconds: row?.estimateSeconds ?? 0,
|
|
113
|
+
...(row?.budgetSeconds !== undefined ? { budgetSeconds: row.budgetSeconds } : {}),
|
|
114
|
+
...(videoSeconds !== undefined ? { videoSeconds } : {}),
|
|
115
|
+
};
|
|
116
|
+
}),
|
|
117
|
+
diagnostics: options.lint ? lintDeck({ project, deck }) : [],
|
|
118
|
+
};
|
|
119
|
+
}
|