@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,851 @@
|
|
|
1
|
+
import { hasErrors } from "../core/diagnostic.ts";
|
|
2
|
+
import { mergeSarif, toSarif } from "../core/sarif.ts";
|
|
3
|
+
import type { CheckCliResult } from "./check.ts";
|
|
4
|
+
import type { ResultFields } from "./contract.ts";
|
|
5
|
+
import type { FlagName, FlagValues } from "./flags.ts";
|
|
6
|
+
import { type FormattedError, formatCreated } from "./format.ts";
|
|
7
|
+
import type { NavResult } from "./goto.ts";
|
|
8
|
+
import type { DeckScope, DecksTarget, ReadableDeck } from "./scope.ts";
|
|
9
|
+
import {
|
|
10
|
+
countSummary,
|
|
11
|
+
displayDiagnostics,
|
|
12
|
+
formatBuild,
|
|
13
|
+
formatCheck,
|
|
14
|
+
formatCues,
|
|
15
|
+
formatInit,
|
|
16
|
+
formatKept,
|
|
17
|
+
formatKeptTheme,
|
|
18
|
+
formatLs,
|
|
19
|
+
formatMarks,
|
|
20
|
+
formatMv,
|
|
21
|
+
formatNav,
|
|
22
|
+
formatNew,
|
|
23
|
+
formatPdf,
|
|
24
|
+
formatRef,
|
|
25
|
+
formatReport,
|
|
26
|
+
formatShot,
|
|
27
|
+
formatShow,
|
|
28
|
+
formatSkipped,
|
|
29
|
+
formatTheme,
|
|
30
|
+
formatVideo,
|
|
31
|
+
formatVoice,
|
|
32
|
+
} from "./text.ts";
|
|
33
|
+
|
|
34
|
+
type ArgKey<S extends string> = S extends `${infer N}?` ? N : S extends `${infer N}...` ? N : S;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The words a command takes, by name: `name` is required, `name?` optional, and `name...`
|
|
38
|
+
* takes every word left, joined by spaces.
|
|
39
|
+
*/
|
|
40
|
+
type Args<A extends readonly string[]> = {
|
|
41
|
+
[S in A[number] as ArgKey<S>]: S extends `${string}?` ? string | undefined : string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type TargetOf<S> = S extends "deck" ? ReadableDeck : S extends "decks" ? DecksTarget : undefined;
|
|
45
|
+
|
|
46
|
+
/** What a command is handed: its arguments by name, its flags typed, and its decks resolved. */
|
|
47
|
+
type Ctx<A extends readonly string[], S extends DeckScope | undefined> = {
|
|
48
|
+
cwd: string;
|
|
49
|
+
flags: FlagValues;
|
|
50
|
+
args: Args<A>;
|
|
51
|
+
/** What `scope` names, resolved: nothing for a command with no scope. */
|
|
52
|
+
target: TargetOf<S>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** A context as the registry keeps it: the arguments as bound, checked by each declaration. */
|
|
56
|
+
type AnyCtx = Omit<Ctx<[], DeckScope | undefined>, "args"> & {
|
|
57
|
+
args: Record<string, string | undefined>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** How a command's result is printed. Only `text` is required. */
|
|
61
|
+
export type Output<D> = {
|
|
62
|
+
text(data: D, color: boolean): string;
|
|
63
|
+
/** What did not happen, for stderr next to the text: the result on stdout stays the result. */
|
|
64
|
+
notes?(data: D, color: boolean): string | undefined;
|
|
65
|
+
/** The `--json` fields next to `ok`; the data as-is when absent. */
|
|
66
|
+
json?(data: D): object;
|
|
67
|
+
/** The data with every path into the source tree as `display` shows it. */
|
|
68
|
+
paths?(data: D, display: (path: string) => string): D;
|
|
69
|
+
/** The error of a run that did its job and found one. */
|
|
70
|
+
failure?(data: D): FormattedError | undefined;
|
|
71
|
+
/** `--format sarif`. */
|
|
72
|
+
sarif?(data: D): object;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type Group = "Development" | "Project" | "Refs" | "Slide" | "Output" | "Help";
|
|
76
|
+
|
|
77
|
+
/** One way to call a command: the words after it and its deck, its flags, and its usage. */
|
|
78
|
+
export type Form = {
|
|
79
|
+
args: readonly string[];
|
|
80
|
+
/** What it takes on top of the global flags, and --deck when it has a scope. */
|
|
81
|
+
flags: readonly FlagName[];
|
|
82
|
+
/** `dekc help <command>`: each way to call it. */
|
|
83
|
+
usage: string[];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type Runs<D> = { run(ctx: AnyCtx): Promise<D> };
|
|
87
|
+
|
|
88
|
+
/** A command's second word, such as `pin` in `dekc voice pin`, with its own words and flags. */
|
|
89
|
+
export type SubSpec<D> = Form & Runs<D>;
|
|
90
|
+
|
|
91
|
+
type Base = Form & {
|
|
92
|
+
summary: string;
|
|
93
|
+
group: Group;
|
|
94
|
+
/** Its lines in `dekc help`: a call, then what it does. */
|
|
95
|
+
overview: Array<readonly [string, string]>;
|
|
96
|
+
/** Its lines in `dekc help --agent`. */
|
|
97
|
+
agent: string[];
|
|
98
|
+
/** The decks it works on, which the CLI resolves; it then takes a deck name and --deck. */
|
|
99
|
+
scope?: DeckScope;
|
|
100
|
+
/** A ref may be its deck: the CLI fetches a pinned ref's missing snapshot first. */
|
|
101
|
+
refs?: true;
|
|
102
|
+
/**
|
|
103
|
+
* A deck whose script.md cannot be read is handed over in `failed` rather than refused, for a
|
|
104
|
+
* command whose answer is why: lint.
|
|
105
|
+
*/
|
|
106
|
+
unreadable?: true;
|
|
107
|
+
/** No word to type: `serve` is the bare `dekc [deck]`. */
|
|
108
|
+
bare?: true;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** A command that prints a result, as text or `--json`. */
|
|
112
|
+
export type ResultSpec<D> = Base &
|
|
113
|
+
Runs<D> & {
|
|
114
|
+
kind: "result";
|
|
115
|
+
output: Output<D>;
|
|
116
|
+
subcommands?: Record<string, SubSpec<D>>;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** A command that keeps running until stopped, and so prints no result. */
|
|
120
|
+
export type SessionSpec = Base & Runs<void> & { kind: "session" };
|
|
121
|
+
|
|
122
|
+
/** `dekc help`, answered before any command runs. */
|
|
123
|
+
export type HelpSpec = Base & { kind: "help" };
|
|
124
|
+
|
|
125
|
+
type Typed<A extends readonly string[], S extends DeckScope | undefined, D> = Omit<Form, "args"> & {
|
|
126
|
+
args: A;
|
|
127
|
+
run(ctx: Ctx<A, S>): Promise<D>;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type Declared<A extends readonly string[], S extends DeckScope | undefined, D> = Omit<
|
|
131
|
+
Base,
|
|
132
|
+
"args" | "scope"
|
|
133
|
+
> &
|
|
134
|
+
Typed<A, S, D> & { scope?: S };
|
|
135
|
+
|
|
136
|
+
// The declarations check each run against its own args and scope; the registry keeps them erased.
|
|
137
|
+
// `J` is what `json` returns, kept so that what a command prints can be held to its contract below.
|
|
138
|
+
function result<
|
|
139
|
+
D,
|
|
140
|
+
const A extends readonly string[],
|
|
141
|
+
S extends DeckScope | undefined = undefined,
|
|
142
|
+
const U extends Record<string, readonly string[]> = Record<never, never>,
|
|
143
|
+
J extends object = never,
|
|
144
|
+
>(
|
|
145
|
+
spec: Declared<A, S, D> & {
|
|
146
|
+
output: Output<D> & { json?(data: D): J };
|
|
147
|
+
subcommands?: { [K in keyof U]: Typed<U[K], S, D> };
|
|
148
|
+
},
|
|
149
|
+
): ResultSpec<D> & Printed<[J] extends [never] ? D : J> {
|
|
150
|
+
return { kind: "result", ...spec } as unknown as ResultSpec<D> &
|
|
151
|
+
Printed<[J] extends [never] ? D : J>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** What a result command prints beside `ok`, as a type alone: nothing carries it at run time. */
|
|
155
|
+
type Printed<P> = { readonly printed?: P };
|
|
156
|
+
|
|
157
|
+
function session<const A extends readonly string[], S extends DeckScope | undefined = undefined>(
|
|
158
|
+
spec: Declared<A, S, void>,
|
|
159
|
+
): SessionSpec {
|
|
160
|
+
return { kind: "session", ...spec } as unknown as SessionSpec;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Lint and check fail on an error diagnostic; everything else reports and succeeds. */
|
|
164
|
+
function diagnosticFailure(
|
|
165
|
+
command: string,
|
|
166
|
+
data: { diagnostics: CheckCliResult["diagnostics"] },
|
|
167
|
+
rerun: string,
|
|
168
|
+
): FormattedError | undefined {
|
|
169
|
+
if (!hasErrors(data.diagnostics)) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
message: `${command} found ${countSummary(data.diagnostics)}`,
|
|
174
|
+
hint: `fix each error in diagnostics, then run \`${rerun}\` again`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A position no page shows is where the next one lands; saying nothing would read as on screen. */
|
|
179
|
+
function navNotes(data: NavResult): string | undefined {
|
|
180
|
+
return data.viewers > 0
|
|
181
|
+
? undefined
|
|
182
|
+
: `no browser shows the deck: the next page opened on the dev server shows ${data.slug}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function withDisplayDiagnostics<D extends { diagnostics: CheckCliResult["diagnostics"] }>(
|
|
186
|
+
data: D,
|
|
187
|
+
display: (path: string) => string,
|
|
188
|
+
): D {
|
|
189
|
+
return { ...data, diagnostics: displayDiagnostics(data.diagnostics, display) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Every command, in the order `dekc help` lists them: the words and flags it takes, its help,
|
|
194
|
+
* what it works on, how it runs, and how its result prints. Each module loads only when its
|
|
195
|
+
* command runs.
|
|
196
|
+
*/
|
|
197
|
+
export const COMMANDS = {
|
|
198
|
+
serve: session({
|
|
199
|
+
bare: true,
|
|
200
|
+
args: ["deck?"],
|
|
201
|
+
flags: ["deck", "remote", "visual", "port"],
|
|
202
|
+
usage: ["dekc [deck] [--visual] [--port N] [--remote]"],
|
|
203
|
+
summary:
|
|
204
|
+
"Start the dev server: skeleton slides, live reload, lint on save, and the presenter view.\nIt keeps running until Ctrl-C.",
|
|
205
|
+
group: "Development",
|
|
206
|
+
overview: [
|
|
207
|
+
[
|
|
208
|
+
"dekc [deck] [--visual] [--port N]",
|
|
209
|
+
"start the dev server; --visual lints overflow/contrast on save",
|
|
210
|
+
],
|
|
211
|
+
["dekc --remote", "share on LAN; presenter notes are password-protected"],
|
|
212
|
+
],
|
|
213
|
+
agent: ["dekc [deck] [--visual] [--port N]", "dekc --remote"],
|
|
214
|
+
run: async ({ cwd, args, flags }) => {
|
|
215
|
+
const { serveCommand } = await import("./serve.ts");
|
|
216
|
+
await serveCommand({
|
|
217
|
+
cwd,
|
|
218
|
+
deck: args.deck ?? flags.deck,
|
|
219
|
+
remote: flags.remote,
|
|
220
|
+
visual: flags.visual,
|
|
221
|
+
port: flags.port,
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
rehearse: session({
|
|
226
|
+
scope: "deck",
|
|
227
|
+
args: ["slug?"],
|
|
228
|
+
flags: ["remote"],
|
|
229
|
+
usage: ["dekc rehearse [deck] [slug] [--remote]"],
|
|
230
|
+
summary: "Play the deck to its Timeline, advancing on its own. Keeps running until Ctrl-C.",
|
|
231
|
+
group: "Development",
|
|
232
|
+
overview: [
|
|
233
|
+
["dekc rehearse [slug]", "auto-advance from a Timeline (no video); --remote shares it"],
|
|
234
|
+
],
|
|
235
|
+
agent: ["dekc rehearse [slug] [--remote]"],
|
|
236
|
+
run: async ({ target, args, flags }) => {
|
|
237
|
+
const { rehearseCommand } = await import("./rehearse.ts");
|
|
238
|
+
await rehearseCommand(target, { slug: args.slug, remote: flags.remote });
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
init: result({
|
|
242
|
+
args: ["dir?"],
|
|
243
|
+
flags: ["deck"],
|
|
244
|
+
usage: ["dekc init [dir] [--deck NAME]"],
|
|
245
|
+
summary:
|
|
246
|
+
"Create a project in dir (default: here), optionally with a first deck.\nNothing that exists is overwritten. Inside a project, add a deck with dekc new.",
|
|
247
|
+
group: "Project",
|
|
248
|
+
overview: [["dekc init [dir] [--deck NAME]", "create a project, optionally with a first deck"]],
|
|
249
|
+
agent: ["dekc init [dir] [--deck NAME]"],
|
|
250
|
+
run: async ({ cwd, args, flags }) => {
|
|
251
|
+
const { initCommand } = await import("./init.ts");
|
|
252
|
+
return initCommand({ cwd, dir: args.dir, deck: flags.deck });
|
|
253
|
+
},
|
|
254
|
+
output: {
|
|
255
|
+
text: formatInit,
|
|
256
|
+
notes: (data, color) => formatKeptTheme(data, color) || undefined,
|
|
257
|
+
paths: (data, display) => ({
|
|
258
|
+
...data,
|
|
259
|
+
created: data.created.map(display),
|
|
260
|
+
updated: data.updated.map(display),
|
|
261
|
+
kept: data.kept.map(display),
|
|
262
|
+
}),
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
new: result({
|
|
266
|
+
args: ["name"],
|
|
267
|
+
flags: ["theme-from"],
|
|
268
|
+
usage: ["dekc new <name> [--theme-from DECK]"],
|
|
269
|
+
summary: "Add a deck with the project's theme, or another deck's, and its skeleton slides.",
|
|
270
|
+
group: "Project",
|
|
271
|
+
overview: [["dekc new <name> [--theme-from DECK]", "add a deck"]],
|
|
272
|
+
agent: ["dekc new <name> [--theme-from DECK]"],
|
|
273
|
+
run: async ({ cwd, args, flags }) => {
|
|
274
|
+
const { newCommand } = await import("./new.ts");
|
|
275
|
+
return newCommand({ cwd, name: args.name, themeFrom: flags["theme-from"] });
|
|
276
|
+
},
|
|
277
|
+
output: {
|
|
278
|
+
text: formatNew,
|
|
279
|
+
paths: (data, display) => ({
|
|
280
|
+
...data,
|
|
281
|
+
created: data.created.map(display),
|
|
282
|
+
updated: data.updated.map(display),
|
|
283
|
+
}),
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
ls: result({
|
|
287
|
+
scope: "decks",
|
|
288
|
+
refs: true,
|
|
289
|
+
args: [],
|
|
290
|
+
flags: [],
|
|
291
|
+
usage: ["dekc ls [deck]"],
|
|
292
|
+
summary: "List the decks, or summarize one: sections, slides, diagnostics, and timing.",
|
|
293
|
+
group: "Project",
|
|
294
|
+
overview: [["dekc ls [deck]", "list decks or show one"]],
|
|
295
|
+
agent: ["dekc ls [deck]"],
|
|
296
|
+
run: async ({ target }) => {
|
|
297
|
+
const { lsCommand } = await import("./ls.ts");
|
|
298
|
+
return lsCommand(target);
|
|
299
|
+
},
|
|
300
|
+
output: {
|
|
301
|
+
text: formatLs,
|
|
302
|
+
json: ({ kind: _kind, ...data }) => data,
|
|
303
|
+
paths: (data, display) =>
|
|
304
|
+
data.kind === "deck"
|
|
305
|
+
? withDisplayDiagnostics(data, display)
|
|
306
|
+
: {
|
|
307
|
+
...withDisplayDiagnostics(data, display),
|
|
308
|
+
decks: data.decks.map((deck) => withDisplayDiagnostics(deck, display)),
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
ref: result({
|
|
313
|
+
args: ["source?"],
|
|
314
|
+
flags: [],
|
|
315
|
+
usage: ["dekc ref <owner/repo/deck>[@rev]", "dekc ref"],
|
|
316
|
+
summary:
|
|
317
|
+
"Pin another project's deck to read as a model, list the pins, or drop one.\nls, show, theme, and shot read a ref as they read a deck.",
|
|
318
|
+
group: "Refs",
|
|
319
|
+
overview: [
|
|
320
|
+
[
|
|
321
|
+
"dekc ref owner/repo/deck[@rev]",
|
|
322
|
+
"pin and fetch one (a GitHub link works too); again moves the pin",
|
|
323
|
+
],
|
|
324
|
+
["dekc ref", "list pinned refs"],
|
|
325
|
+
["dekc ref rm <ref>", "drop one"],
|
|
326
|
+
],
|
|
327
|
+
agent: [
|
|
328
|
+
"dekc ref [owner/repo/deck[@rev] | github-link] pin + fetch; no args lists; dekc ref rm <ref>",
|
|
329
|
+
],
|
|
330
|
+
run: async ({ cwd, args }) => {
|
|
331
|
+
const { addRef, listRefs } = await import("./ref.ts");
|
|
332
|
+
return args.source === undefined ? listRefs(cwd) : addRef(cwd, args.source);
|
|
333
|
+
},
|
|
334
|
+
subcommands: {
|
|
335
|
+
rm: {
|
|
336
|
+
args: ["ref"],
|
|
337
|
+
flags: [],
|
|
338
|
+
usage: ["dekc ref rm <ref>"],
|
|
339
|
+
run: async ({ cwd, args }) => {
|
|
340
|
+
const { removeRef } = await import("./ref.ts");
|
|
341
|
+
return removeRef(cwd, args.ref);
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
output: { text: formatRef },
|
|
346
|
+
}),
|
|
347
|
+
show: result({
|
|
348
|
+
scope: "deck",
|
|
349
|
+
refs: true,
|
|
350
|
+
args: ["slug"],
|
|
351
|
+
flags: [],
|
|
352
|
+
usage: ["dekc show [deck] <slug>"],
|
|
353
|
+
summary: "Print what one slide is made of: its script, HTML, CSS, TS, theme rules, and assets.",
|
|
354
|
+
group: "Slide",
|
|
355
|
+
overview: [["dekc show <slug>", "print a slide's script, HTML, CSS, TS, theme rules, assets"]],
|
|
356
|
+
agent: [
|
|
357
|
+
"dekc show <slug> script with its beat ids, HTML, CSS, TS, the theme rules it uses, assets",
|
|
358
|
+
],
|
|
359
|
+
run: async ({ target, args }) => {
|
|
360
|
+
const { showCommand } = await import("./show.ts");
|
|
361
|
+
return showCommand(target, args.slug);
|
|
362
|
+
},
|
|
363
|
+
output: { text: formatShow },
|
|
364
|
+
}),
|
|
365
|
+
theme: result({
|
|
366
|
+
scope: "deck",
|
|
367
|
+
refs: true,
|
|
368
|
+
args: ["layout?"],
|
|
369
|
+
flags: [],
|
|
370
|
+
usage: ["dekc theme [deck] [layout]"],
|
|
371
|
+
summary: "List the theme's layouts, classes, and tokens, or print one layout's markup.",
|
|
372
|
+
group: "Slide",
|
|
373
|
+
overview: [
|
|
374
|
+
[
|
|
375
|
+
"dekc theme [layout]",
|
|
376
|
+
"list the deck theme's layouts, classes, and tokens; print a layout's markup",
|
|
377
|
+
],
|
|
378
|
+
],
|
|
379
|
+
agent: [
|
|
380
|
+
"dekc theme [layout] deck theme: layouts, classes, tokens; with a layout, its example markup",
|
|
381
|
+
],
|
|
382
|
+
run: async ({ target, args }) => {
|
|
383
|
+
const { themeCommand } = await import("./theme.ts");
|
|
384
|
+
return themeCommand(target, args.layout);
|
|
385
|
+
},
|
|
386
|
+
output: {
|
|
387
|
+
text: formatTheme,
|
|
388
|
+
paths: (data, display) => ({ ...data, path: display(data.path) }),
|
|
389
|
+
},
|
|
390
|
+
}),
|
|
391
|
+
check: result({
|
|
392
|
+
scope: "deck",
|
|
393
|
+
args: ["slug"],
|
|
394
|
+
flags: ["shot", "voice"],
|
|
395
|
+
usage: ["dekc check [deck] <slug> [--shot] [--voice]"],
|
|
396
|
+
summary: "Lint one slide, rendered rules included when Playwright is installed.",
|
|
397
|
+
group: "Slide",
|
|
398
|
+
overview: [
|
|
399
|
+
["dekc check <slug>", "lint one slide; --shot adds a screenshot; --voice adds readings"],
|
|
400
|
+
],
|
|
401
|
+
agent: ["dekc check <slug> [--shot] [--voice] fill: share of the frame it fills, by tenths"],
|
|
402
|
+
run: async ({ target, args, flags }) => {
|
|
403
|
+
const { checkCommand } = await import("./check.ts");
|
|
404
|
+
return checkCommand(target, { slug: args.slug, shot: flags.shot, voice: flags.voice });
|
|
405
|
+
},
|
|
406
|
+
output: {
|
|
407
|
+
text: formatCheck,
|
|
408
|
+
notes: (data, color) => formatSkipped(data.skipped, color) || undefined,
|
|
409
|
+
paths: withDisplayDiagnostics,
|
|
410
|
+
failure: (data) => diagnosticFailure("check", data, `dekc check ${data.slug}`),
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
shot: result({
|
|
414
|
+
scope: "deck",
|
|
415
|
+
refs: true,
|
|
416
|
+
args: ["slug?"],
|
|
417
|
+
flags: ["step", "to", "at", "sheet", "motion"],
|
|
418
|
+
usage: [
|
|
419
|
+
"dekc shot [deck] [slug] [--step ID|N]",
|
|
420
|
+
"dekc shot [deck] --sheet",
|
|
421
|
+
"dekc shot [deck] <slug> --motion [--step ID|N]",
|
|
422
|
+
"dekc shot [deck] <a> --to <b> [--at 0..1]",
|
|
423
|
+
],
|
|
424
|
+
summary:
|
|
425
|
+
"Screenshot slides at their last beat, tile the deck on contact sheets, lay out a slide's\nbeats in motion, or take one frame of the transition from a to b.",
|
|
426
|
+
group: "Slide",
|
|
427
|
+
overview: [
|
|
428
|
+
["dekc shot [slug]", "write screenshots; --step <id|n> picks a beat"],
|
|
429
|
+
["dekc shot --sheet", "tile every slide on one image, to judge the deck in one look"],
|
|
430
|
+
["dekc shot <slug> --motion", "each beat held at moments through its motion, on one image"],
|
|
431
|
+
["dekc shot <a> --to <b> [--at 0.5]", "freeze the transition from a into b (morph check)"],
|
|
432
|
+
],
|
|
433
|
+
agent: [
|
|
434
|
+
"dekc shot [slug] [--step <id|n>]",
|
|
435
|
+
"dekc shot --sheet every slide at its last beat on contact sheets sized for one look",
|
|
436
|
+
"dekc shot <slug> --motion [--step <id|n>] a row per beat: 0/25/50/75% of all it moves, then its end",
|
|
437
|
+
"dekc shot <a> --to <b> [--at 0..1] frame of the a→b view transition, default 0.5",
|
|
438
|
+
],
|
|
439
|
+
run: async ({ target, args, flags }) => {
|
|
440
|
+
const { shotCommand } = await import("./shot.ts");
|
|
441
|
+
return shotCommand(target, {
|
|
442
|
+
slug: args.slug,
|
|
443
|
+
step: flags.step,
|
|
444
|
+
to: flags.to,
|
|
445
|
+
at: flags.at,
|
|
446
|
+
sheet: flags.sheet,
|
|
447
|
+
motion: flags.motion,
|
|
448
|
+
});
|
|
449
|
+
},
|
|
450
|
+
output: { text: formatShot },
|
|
451
|
+
}),
|
|
452
|
+
mv: result({
|
|
453
|
+
scope: "deck",
|
|
454
|
+
args: ["old", "new?"],
|
|
455
|
+
flags: ["before", "after"],
|
|
456
|
+
usage: ["dekc mv [deck] <old> <new>", "dekc mv [deck] <slug> --before|--after <slug>"],
|
|
457
|
+
summary: "Rename a section and every file named after it, or move it in the script.",
|
|
458
|
+
group: "Slide",
|
|
459
|
+
overview: [
|
|
460
|
+
["dekc mv <old> <new>", "rename a section id and its HTML"],
|
|
461
|
+
["dekc mv <slug> --before|--after <slug>", "reorder a section"],
|
|
462
|
+
],
|
|
463
|
+
agent: ["dekc mv <old> <new> | dekc mv <slug> --before|--after <slug>"],
|
|
464
|
+
run: async ({ target, args, flags }) => {
|
|
465
|
+
const { mvCommand } = await import("./mv.ts");
|
|
466
|
+
return mvCommand(target, {
|
|
467
|
+
slug: args.old,
|
|
468
|
+
to: args.new,
|
|
469
|
+
before: flags.before,
|
|
470
|
+
after: flags.after,
|
|
471
|
+
});
|
|
472
|
+
},
|
|
473
|
+
output: { text: formatMv },
|
|
474
|
+
}),
|
|
475
|
+
goto: result({
|
|
476
|
+
scope: "deck",
|
|
477
|
+
args: ["slug"],
|
|
478
|
+
flags: [],
|
|
479
|
+
usage: ["dekc goto [deck] <slug>"],
|
|
480
|
+
summary: "Move the open browser to a slide. Needs a running dev server.",
|
|
481
|
+
group: "Slide",
|
|
482
|
+
overview: [["dekc goto <slug>", "jump the open browser"]],
|
|
483
|
+
agent: ["dekc goto <slug> requires running dek"],
|
|
484
|
+
run: async ({ target, args }) => {
|
|
485
|
+
const { gotoCommand } = await import("./goto.ts");
|
|
486
|
+
return gotoCommand(target, args.slug);
|
|
487
|
+
},
|
|
488
|
+
output: { text: formatNav, notes: navNotes },
|
|
489
|
+
}),
|
|
490
|
+
current: result({
|
|
491
|
+
scope: "deck",
|
|
492
|
+
args: [],
|
|
493
|
+
flags: [],
|
|
494
|
+
usage: ["dekc current [deck]"],
|
|
495
|
+
summary: "Print the slide on screen. Needs a running dev server.",
|
|
496
|
+
group: "Slide",
|
|
497
|
+
overview: [["dekc current", "print the slide on screen"]],
|
|
498
|
+
agent: ["dekc current requires running dek"],
|
|
499
|
+
run: async ({ target }) => {
|
|
500
|
+
const { currentCommand } = await import("./goto.ts");
|
|
501
|
+
return currentCommand(target);
|
|
502
|
+
},
|
|
503
|
+
output: { text: formatNav, notes: navNotes },
|
|
504
|
+
}),
|
|
505
|
+
marks: result({
|
|
506
|
+
scope: "deck",
|
|
507
|
+
args: [],
|
|
508
|
+
flags: [],
|
|
509
|
+
usage: ["dekc marks [deck]"],
|
|
510
|
+
summary:
|
|
511
|
+
"List the beats marked to rewrite in the presenter view, by pressing m while rehearsing:\nwhere each is in script.md, what it said when marked, and what it says now.",
|
|
512
|
+
group: "Slide",
|
|
513
|
+
overview: [
|
|
514
|
+
["dekc marks", "list the beats marked in the presenter view (m) to rewrite"],
|
|
515
|
+
["dekc marks clear", "drop the deck's marks"],
|
|
516
|
+
],
|
|
517
|
+
agent: [
|
|
518
|
+
"dekc marks [clear] beats the speaker marked (m) to rewrite: line, was, text, status open|edited|gone",
|
|
519
|
+
],
|
|
520
|
+
run: async ({ target }) => {
|
|
521
|
+
const { marksCommand } = await import("./marks.ts");
|
|
522
|
+
return marksCommand(target);
|
|
523
|
+
},
|
|
524
|
+
subcommands: {
|
|
525
|
+
clear: {
|
|
526
|
+
args: [],
|
|
527
|
+
flags: [],
|
|
528
|
+
usage: ["dekc marks [deck] clear"],
|
|
529
|
+
run: async ({ target }) => {
|
|
530
|
+
const { clearMarksCommand } = await import("./marks.ts");
|
|
531
|
+
return clearMarksCommand(target);
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
output: {
|
|
536
|
+
text: formatMarks,
|
|
537
|
+
paths: (data, display) =>
|
|
538
|
+
data.action === "list"
|
|
539
|
+
? { ...data, marks: data.marks.map((mark) => ({ ...mark, path: display(mark.path) })) }
|
|
540
|
+
: data,
|
|
541
|
+
},
|
|
542
|
+
}),
|
|
543
|
+
sync: result({
|
|
544
|
+
scope: "decks",
|
|
545
|
+
args: [],
|
|
546
|
+
flags: [],
|
|
547
|
+
usage: ["dekc sync [deck]"],
|
|
548
|
+
summary:
|
|
549
|
+
"Create a skeleton for each section without slide HTML, refresh the skeletons\nnobody has edited since, and remove the ones whose section is gone.",
|
|
550
|
+
group: "Slide",
|
|
551
|
+
overview: [["dekc sync", "create missing skeletons; refresh or drop the ones nobody edited"]],
|
|
552
|
+
agent: [
|
|
553
|
+
"dekc sync create missing skeletons; refresh or drop untouched ones; never edits your slides",
|
|
554
|
+
],
|
|
555
|
+
run: async ({ target }) => {
|
|
556
|
+
const { syncCommand } = await import("./sync.ts");
|
|
557
|
+
return syncCommand(target);
|
|
558
|
+
},
|
|
559
|
+
output: {
|
|
560
|
+
text: (data) => formatCreated(data.created, data.updated, data.removed),
|
|
561
|
+
notes: (data, color) => formatKept(data.kept, color) || undefined,
|
|
562
|
+
paths: (data, display) => ({
|
|
563
|
+
created: data.created.map(display),
|
|
564
|
+
updated: data.updated.map(display),
|
|
565
|
+
removed: data.removed.map(display),
|
|
566
|
+
kept: data.kept.map(display),
|
|
567
|
+
}),
|
|
568
|
+
},
|
|
569
|
+
}),
|
|
570
|
+
lint: result({
|
|
571
|
+
scope: "decks",
|
|
572
|
+
unreadable: true,
|
|
573
|
+
args: [],
|
|
574
|
+
flags: ["fix", "visual", "format"],
|
|
575
|
+
usage: ["dekc lint [deck] [--fix] [--visual] [--format sarif]"],
|
|
576
|
+
summary: "Check the script against the slides. Errors exit 1; warnings alone do not.",
|
|
577
|
+
group: "Output",
|
|
578
|
+
overview: [
|
|
579
|
+
["dekc lint [--fix]", "check script.md against slides; --fix syncs first"],
|
|
580
|
+
["dekc lint --visual", "add overflow and contrast rules"],
|
|
581
|
+
["dekc lint --format sarif", "print diagnostics as SARIF, for code scanning"],
|
|
582
|
+
],
|
|
583
|
+
agent: ["dekc lint [--fix] [--visual] [--format sarif]"],
|
|
584
|
+
run: async ({ cwd, target, flags }) => {
|
|
585
|
+
const { lintCommand } = await import("./lint.ts");
|
|
586
|
+
return lintCommand(target, { cwd, fix: flags.fix, visual: flags.visual });
|
|
587
|
+
},
|
|
588
|
+
output: {
|
|
589
|
+
text: (data, color) => formatReport(data.diagnostics, { color }),
|
|
590
|
+
notes: (data, color) => formatSkipped(data.skipped, color) || undefined,
|
|
591
|
+
json: ({ rumdlSarif: _rumdlSarif, ...data }) => data,
|
|
592
|
+
paths: withDisplayDiagnostics,
|
|
593
|
+
failure: (data) => diagnosticFailure("lint", data, "dekc lint"),
|
|
594
|
+
sarif: (data) => {
|
|
595
|
+
const dek = data.diagnostics.filter((diagnostic) => diagnostic.id.startsWith("DEK"));
|
|
596
|
+
return mergeSarif(toSarif(dek, { skipped: data.skipped ?? [] }), data.rumdlSarif);
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
}),
|
|
600
|
+
cues: result({
|
|
601
|
+
scope: "deck",
|
|
602
|
+
args: [],
|
|
603
|
+
flags: [],
|
|
604
|
+
usage: ["dekc cues [deck]"],
|
|
605
|
+
summary: "Print the spoken paragraphs as Cue[]. No voice engine needed.",
|
|
606
|
+
group: "Output",
|
|
607
|
+
overview: [["dekc cues", "print spoken paragraphs as Cue[]"]],
|
|
608
|
+
agent: ["dekc cues"],
|
|
609
|
+
run: async ({ target }) => {
|
|
610
|
+
const { cuesCommand } = await import("./cues.ts");
|
|
611
|
+
return cuesCommand(target);
|
|
612
|
+
},
|
|
613
|
+
output: {
|
|
614
|
+
text: formatCues,
|
|
615
|
+
paths: withDisplayDiagnostics,
|
|
616
|
+
},
|
|
617
|
+
}),
|
|
618
|
+
voice: result({
|
|
619
|
+
scope: "deck",
|
|
620
|
+
args: [],
|
|
621
|
+
flags: [],
|
|
622
|
+
usage: ["dekc voice [deck]"],
|
|
623
|
+
summary:
|
|
624
|
+
"Synthesize the sentences that changed, list speakers, speak one sentence, add a reading,\nor pin the audio and timeline.",
|
|
625
|
+
group: "Output",
|
|
626
|
+
overview: [
|
|
627
|
+
["dekc voice", "synthesize changed sentences"],
|
|
628
|
+
["dekc voice speakers", "list engine speakers"],
|
|
629
|
+
["dekc voice say <text>", "speak one sentence"],
|
|
630
|
+
["dekc voice dict add <word> <kana> [--accent N]", "add a reading"],
|
|
631
|
+
["dekc voice pin", "pin TTS master.wav + timeline.json"],
|
|
632
|
+
],
|
|
633
|
+
agent: ["dekc voice [speakers | say <text> | dict add <word> <kana> [--accent N] | pin]"],
|
|
634
|
+
run: async ({ target }) => {
|
|
635
|
+
const { synthVoice } = await import("./voice.ts");
|
|
636
|
+
return synthVoice(target);
|
|
637
|
+
},
|
|
638
|
+
subcommands: {
|
|
639
|
+
speakers: {
|
|
640
|
+
args: [],
|
|
641
|
+
flags: [],
|
|
642
|
+
usage: ["dekc voice [deck] speakers"],
|
|
643
|
+
run: async ({ target }) => {
|
|
644
|
+
const { listSpeakers } = await import("./voice.ts");
|
|
645
|
+
return listSpeakers(target);
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
say: {
|
|
649
|
+
args: ["text..."],
|
|
650
|
+
flags: [],
|
|
651
|
+
usage: ["dekc voice [deck] say <text>"],
|
|
652
|
+
run: async ({ target, args }) => {
|
|
653
|
+
const { sayVoice } = await import("./voice.ts");
|
|
654
|
+
return sayVoice(target, args.text);
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
"dict add": {
|
|
658
|
+
args: ["word", "kana"],
|
|
659
|
+
flags: ["accent"],
|
|
660
|
+
usage: ["dekc voice [deck] dict add <word> <kana> [--accent N]"],
|
|
661
|
+
run: async ({ target, args, flags }) => {
|
|
662
|
+
const { addReading } = await import("./voice.ts");
|
|
663
|
+
return addReading(target, { word: args.word, kana: args.kana, accent: flags.accent });
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
pin: {
|
|
667
|
+
args: [],
|
|
668
|
+
flags: [],
|
|
669
|
+
usage: ["dekc voice [deck] pin"],
|
|
670
|
+
run: async ({ target }) => {
|
|
671
|
+
const { pinVoice } = await import("./voice.ts");
|
|
672
|
+
return pinVoice(target);
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
output: { text: formatVoice },
|
|
677
|
+
}),
|
|
678
|
+
build: result({
|
|
679
|
+
scope: "decks",
|
|
680
|
+
args: [],
|
|
681
|
+
flags: ["root-dist", "url", "public"],
|
|
682
|
+
usage: ["dekc build [deck] [--root-dist] [--url <url>] [--public]"],
|
|
683
|
+
summary:
|
|
684
|
+
"Write the whole talk into one HTML file, dist/<deck>.html. Lint never stops a build;\nthe output says what lint found. Given the URL dist/ is served from, the first slide\nalso becomes dist/<deck>.png, the picture a shared link shows.",
|
|
685
|
+
group: "Output",
|
|
686
|
+
overview: [["dekc build [--root-dist] [--url <url>] [--public]", "write a single HTML file"]],
|
|
687
|
+
agent: ["dekc build [--root-dist] [--url <url>] [--public]"],
|
|
688
|
+
run: async ({ target, flags }) => {
|
|
689
|
+
const { buildCommand } = await import("./build.ts");
|
|
690
|
+
return buildCommand(target, {
|
|
691
|
+
rootDist: flags["root-dist"],
|
|
692
|
+
url: flags.url,
|
|
693
|
+
public: flags.public,
|
|
694
|
+
});
|
|
695
|
+
},
|
|
696
|
+
output: {
|
|
697
|
+
text: formatBuild,
|
|
698
|
+
notes: (data, color) => formatSkipped(data.skipped, color) || undefined,
|
|
699
|
+
paths: withDisplayDiagnostics,
|
|
700
|
+
},
|
|
701
|
+
}),
|
|
702
|
+
video: result({
|
|
703
|
+
scope: "deck",
|
|
704
|
+
args: ["slug?"],
|
|
705
|
+
flags: ["fps", "root-dist"],
|
|
706
|
+
usage: ["dekc video [deck] [slug] [--fps N] [--root-dist]"],
|
|
707
|
+
summary: "Bake dist/<deck>.mp4 from the Timeline, or one slide into .cache/video/.",
|
|
708
|
+
group: "Output",
|
|
709
|
+
overview: [
|
|
710
|
+
[
|
|
711
|
+
"dekc video [slug] [--fps N] [--root-dist]",
|
|
712
|
+
"bake dist/<deck>.mp4 (or one slide under .cache/video/)",
|
|
713
|
+
],
|
|
714
|
+
],
|
|
715
|
+
agent: ["dekc video [slug] [--fps N] [--root-dist]"],
|
|
716
|
+
run: async ({ target, args, flags }) => {
|
|
717
|
+
const { videoCommand } = await import("./video.ts");
|
|
718
|
+
return videoCommand(target, {
|
|
719
|
+
slug: args.slug,
|
|
720
|
+
fps: flags.fps,
|
|
721
|
+
rootDist: flags["root-dist"],
|
|
722
|
+
});
|
|
723
|
+
},
|
|
724
|
+
output: { text: formatVideo },
|
|
725
|
+
}),
|
|
726
|
+
pdf: result({
|
|
727
|
+
scope: "decks",
|
|
728
|
+
args: [],
|
|
729
|
+
flags: ["root-dist"],
|
|
730
|
+
usage: ["dekc pdf [deck] [--root-dist]"],
|
|
731
|
+
summary: "Write dist/<deck>.pdf, one page per slide at its last beat. Needs Playwright.",
|
|
732
|
+
group: "Output",
|
|
733
|
+
overview: [["dekc pdf [--root-dist]", "write a PDF"]],
|
|
734
|
+
agent: ["dekc pdf [--root-dist]"],
|
|
735
|
+
run: async ({ target, flags }) => {
|
|
736
|
+
const { pdfCommand } = await import("./pdf.ts");
|
|
737
|
+
return pdfCommand(target, { rootDist: flags["root-dist"] });
|
|
738
|
+
},
|
|
739
|
+
output: { text: formatPdf },
|
|
740
|
+
}),
|
|
741
|
+
help: {
|
|
742
|
+
kind: "help",
|
|
743
|
+
args: ["command?"],
|
|
744
|
+
flags: ["agent"],
|
|
745
|
+
usage: ["dekc help [command] [--agent]"],
|
|
746
|
+
summary: "Show every command, or one command's usage and flags.",
|
|
747
|
+
group: "Help",
|
|
748
|
+
overview: [["dekc help [command]", "show this help, or one command's usage and flags"]],
|
|
749
|
+
agent: ["dekc help [command] | dekc <command> --help | dekc --version"],
|
|
750
|
+
} satisfies HelpSpec as HelpSpec,
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
type Specs = typeof COMMANDS;
|
|
754
|
+
|
|
755
|
+
export type CommandName = keyof Specs;
|
|
756
|
+
|
|
757
|
+
/** The commands that print a result. */
|
|
758
|
+
export type ResultCommand = {
|
|
759
|
+
[K in CommandName]: Specs[K] extends { kind: "result" } ? K : never;
|
|
760
|
+
}[CommandName];
|
|
761
|
+
|
|
762
|
+
type DataOf<K extends ResultCommand> = Specs[K] extends ResultSpec<infer D> ? D : never;
|
|
763
|
+
|
|
764
|
+
/** One command's result, before it is printed. */
|
|
765
|
+
export type CliResult = { [K in ResultCommand]: { command: K; data: DataOf<K> } }[ResultCommand];
|
|
766
|
+
|
|
767
|
+
type PrintedOf<K extends ResultCommand> = NonNullable<Specs[K]["printed"]>;
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Every command prints what its contract names. A command whose data outgrew its contract fails
|
|
771
|
+
* to compile here, by name; the tests hold the printed lines to the contract field by field.
|
|
772
|
+
*/
|
|
773
|
+
type BreaksContract = {
|
|
774
|
+
[K in ResultCommand]: PrintedOf<K> extends ResultFields[K] ? never : K;
|
|
775
|
+
}[ResultCommand];
|
|
776
|
+
const keepsContract: [BreaksContract] extends [never] ? true : BreaksContract = true;
|
|
777
|
+
void keepsContract;
|
|
778
|
+
|
|
779
|
+
export type AnySpec = ResultSpec<unknown> | SessionSpec | HelpSpec;
|
|
780
|
+
|
|
781
|
+
/** The spec of a command, or nothing for a word that is none. */
|
|
782
|
+
function commandSpec(word: string | undefined): AnySpec | undefined {
|
|
783
|
+
return word !== undefined && Object.hasOwn(COMMANDS, word)
|
|
784
|
+
? (COMMANDS[word as CommandName] as AnySpec)
|
|
785
|
+
: undefined;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Every spec, in the order `dekc help` lists them. */
|
|
789
|
+
export function allSpecs(): Array<[CommandName, AnySpec]> {
|
|
790
|
+
return Object.entries(COMMANDS) as Array<[CommandName, AnySpec]>;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** A word the user may type as a command: every command but the bare serve. */
|
|
794
|
+
export function isTypedCommand(word: string | undefined): word is CommandName {
|
|
795
|
+
const spec = commandSpec(word);
|
|
796
|
+
return spec !== undefined && spec.bare !== true;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export function subcommandsOf(spec: AnySpec): Record<string, SubSpec<unknown>> {
|
|
800
|
+
return spec.kind === "result" ? (spec.subcommands ?? {}) : {};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* The subcommand a command line names, and the index of its first word in `positionals`
|
|
805
|
+
* (the command word is 0). It follows the command, or the deck when the command takes one.
|
|
806
|
+
*/
|
|
807
|
+
export function subcommandAt(
|
|
808
|
+
name: CommandName,
|
|
809
|
+
positionals: string[],
|
|
810
|
+
): { name: string; at: number } | undefined {
|
|
811
|
+
const spec = COMMANDS[name] as AnySpec;
|
|
812
|
+
const starts = spec.scope ? [1, 2] : [1];
|
|
813
|
+
for (const at of starts) {
|
|
814
|
+
for (const sub of Object.keys(subcommandsOf(spec))) {
|
|
815
|
+
const words = sub.split(" ");
|
|
816
|
+
if (words.every((word, index) => positionals[at + index] === word)) {
|
|
817
|
+
return { name: sub, at };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** The form a command line runs: a subcommand's, or the command's own. */
|
|
825
|
+
export function formOf(name: CommandName, sub?: string): Form & Partial<Runs<unknown>> {
|
|
826
|
+
const spec = COMMANDS[name] as AnySpec;
|
|
827
|
+
return (sub === undefined ? undefined : subcommandsOf(spec)[sub]) ?? spec;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** The flags a form takes on top of the global ones: --deck with a scope, then its own. */
|
|
831
|
+
export function acceptedFlags(name: CommandName, sub?: string): FlagName[] {
|
|
832
|
+
const spec = COMMANDS[name] as AnySpec;
|
|
833
|
+
return [...(spec.scope ? (["deck"] as const) : []), ...formOf(name, sub).flags];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Every flag a command takes in any of its forms, for its help. */
|
|
837
|
+
export function commandFlags(name: CommandName): FlagName[] {
|
|
838
|
+
const subs = Object.keys(subcommandsOf(COMMANDS[name] as AnySpec));
|
|
839
|
+
return [...new Set([acceptedFlags(name), ...subs.map((sub) => acceptedFlags(name, sub))].flat())];
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Every usage line of a command, its subcommands' included. */
|
|
843
|
+
export function usageLines(name: CommandName): string[] {
|
|
844
|
+
const spec = COMMANDS[name] as AnySpec;
|
|
845
|
+
return [spec.usage, ...Object.values(subcommandsOf(spec)).map((sub) => sub.usage)].flat();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** How the result of `command` prints. */
|
|
849
|
+
export function outputOf(command: ResultCommand): Output<unknown> {
|
|
850
|
+
return (COMMANDS[command] as ResultSpec<unknown>).output;
|
|
851
|
+
}
|