@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,161 @@
|
|
|
1
|
+
import type { Diagnostic, SkippedCheck } from "../core/diagnostic.ts";
|
|
2
|
+
import { DekError } from "../core/error.ts";
|
|
3
|
+
import { PLAYWRIGHT_INSTALL, type PlaywrightRunner, playwrightReady } from "../core/playwright.ts";
|
|
4
|
+
import { addressHint, deckAround, type HintDeck } from "./address.ts";
|
|
5
|
+
import { type CliResult, outputOf } from "./commands.ts";
|
|
6
|
+
import { displayPath, formatError, formatErrorText } from "./format.ts";
|
|
7
|
+
import { shouldColor, terminalSafe } from "./tty.ts";
|
|
8
|
+
|
|
9
|
+
export type { CliResult, SkippedCheck };
|
|
10
|
+
|
|
11
|
+
/** `{ skipped }` when any check was skipped, nothing otherwise: the field is present only when it matters. */
|
|
12
|
+
export function skippedChecks(skipped: SkippedCheck[]): { skipped?: SkippedCheck[] } {
|
|
13
|
+
return skipped.length > 0 ? { skipped } : {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The overflow and contrast rules, when a run leaves them out: why, and `rerun`, the command that
|
|
18
|
+
* measures them. Without Playwright, installing it comes first, whatever the reason was.
|
|
19
|
+
*/
|
|
20
|
+
export function visualSkipped(
|
|
21
|
+
rerun: string,
|
|
22
|
+
options: { reason: string; runner?: PlaywrightRunner },
|
|
23
|
+
): SkippedCheck {
|
|
24
|
+
const measure = `run \`${rerun}\` to measure overflow and contrast`;
|
|
25
|
+
return playwrightReady(options.runner)
|
|
26
|
+
? { check: "visual", reason: options.reason, hint: measure }
|
|
27
|
+
: {
|
|
28
|
+
check: "visual",
|
|
29
|
+
reason: "Playwright is not installed",
|
|
30
|
+
hint: `${PLAYWRIGHT_INSTALL}, then ${measure}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type WriteSuccessOptions = {
|
|
35
|
+
json: boolean;
|
|
36
|
+
format?: string;
|
|
37
|
+
/** Print diagnostic paths relative to this directory. SARIF keeps absolute URIs. */
|
|
38
|
+
cwd?: string;
|
|
39
|
+
/** The one deck the command worked on, which hints that name no path are about. */
|
|
40
|
+
deck?: HintDeck;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Where a run's hints are read: the directory it ran in, and the deck it worked on, if one. */
|
|
44
|
+
export type HintSite = { cwd: string; deck?: HintDeck };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A result whose hints run as written from where the command ran: see `addressHint`. A diagnostic
|
|
48
|
+
* is about the deck its path is in; a skipped check, about the deck the command worked on.
|
|
49
|
+
*/
|
|
50
|
+
export function addressResult(result: CliResult, site: HintSite): CliResult {
|
|
51
|
+
const data = result.data as { diagnostics?: Diagnostic[]; skipped?: SkippedCheck[] };
|
|
52
|
+
if (data.diagnostics === undefined && data.skipped === undefined) {
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
const address = <T extends { hint?: string }>(entry: T, deck: HintDeck | undefined): T =>
|
|
56
|
+
entry.hint === undefined ? entry : { ...entry, hint: addressHint(entry.hint, deck, site.cwd) };
|
|
57
|
+
return {
|
|
58
|
+
...result,
|
|
59
|
+
data: {
|
|
60
|
+
...data,
|
|
61
|
+
...(data.diagnostics && {
|
|
62
|
+
diagnostics: data.diagnostics.map((diagnostic) =>
|
|
63
|
+
address(diagnostic, diagnosticDeck(diagnostic) ?? site.deck),
|
|
64
|
+
),
|
|
65
|
+
}),
|
|
66
|
+
...(data.skipped && { skipped: data.skipped.map((entry) => address(entry, site.deck)) }),
|
|
67
|
+
},
|
|
68
|
+
} as CliResult;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function diagnosticDeck(diagnostic: Diagnostic): HintDeck | undefined {
|
|
72
|
+
return diagnostic.path === undefined ? undefined : deckAround(diagnostic.path);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Print a result: SARIF, the `--json` envelope, or text with what did not run on stderr.
|
|
77
|
+
* Only lint and check fail on an error diagnostic; a build or a listing reports what lint
|
|
78
|
+
* would say but still did its job: at the venue, a deck that shows is better than none. The
|
|
79
|
+
* error has the shape a thrown one has, so `--json` readers branch on `ok` and read `error` alike.
|
|
80
|
+
*/
|
|
81
|
+
export function writeSuccess(original: CliResult, options: WriteSuccessOptions): void {
|
|
82
|
+
const output = outputOf(original.command);
|
|
83
|
+
const { cwd } = options;
|
|
84
|
+
const addressed = cwd === undefined ? original : addressResult(original, { cwd, ...options });
|
|
85
|
+
const result = cwd === undefined ? addressed : displayPaths(addressed, cwd);
|
|
86
|
+
const failed = output.failure?.(result.data);
|
|
87
|
+
const failure =
|
|
88
|
+
failed?.hint === undefined || cwd === undefined
|
|
89
|
+
? failed
|
|
90
|
+
: { ...failed, hint: addressHint(failed.hint, options.deck, cwd) };
|
|
91
|
+
if (options.format === "sarif" && output.sarif) {
|
|
92
|
+
process.stdout.write(`${JSON.stringify(output.sarif(addressed.data))}\n`);
|
|
93
|
+
} else if (options.json) {
|
|
94
|
+
const envelope = failure ? { ok: false, error: failure } : { ok: true };
|
|
95
|
+
const data = output.json ? output.json(result.data) : (result.data as object);
|
|
96
|
+
process.stdout.write(`${JSON.stringify({ ...envelope, ...data })}\n`);
|
|
97
|
+
} else {
|
|
98
|
+
const text = formatText(result, shouldColor(process.stdout));
|
|
99
|
+
process.stdout.write(`${terminalSafe(text)}\n`);
|
|
100
|
+
const notes = output.notes?.(result.data, shouldColor(process.stderr));
|
|
101
|
+
if (notes) {
|
|
102
|
+
process.stderr.write(`${terminalSafe(notes)}\n`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (failure) {
|
|
107
|
+
// Not process.exit: a pipe still holds the output, and exit would cut it at 64 KB.
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Paths into the source tree (diagnostics, files dek created) relative to
|
|
114
|
+
* `cwd`, so an agent reads `slides/intro.html` instead of the same long
|
|
115
|
+
* absolute prefix on every line. Artifacts dek writes, such as `shot` or a
|
|
116
|
+
* build, stay absolute: they are meant to be opened as-is.
|
|
117
|
+
*/
|
|
118
|
+
export function displayPaths(result: CliResult, cwd: string): CliResult {
|
|
119
|
+
const paths = outputOf(result.command).paths;
|
|
120
|
+
if (!paths) {
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
return { ...result, data: paths(result.data, (path) => displayPath(path, cwd)) } as CliResult;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A result as text, without what did not run; see writeSuccess. */
|
|
127
|
+
export function formatText(result: CliResult, color = false): string {
|
|
128
|
+
return outputOf(result.command).text(result.data, color);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Print why a command line failed, in the shape a failed result has: `--json` readers get
|
|
133
|
+
* `{ ok: false, error }` on stdout, everyone else the error and its hint on stderr.
|
|
134
|
+
*/
|
|
135
|
+
export function writeFailure(error: unknown, options: { json: boolean; cwd: string }): void {
|
|
136
|
+
error = addressError(error, options.cwd);
|
|
137
|
+
if (options.json) {
|
|
138
|
+
const formatted = formatError(error, { cwd: options.cwd });
|
|
139
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: formatted })}\n`);
|
|
140
|
+
} else {
|
|
141
|
+
const color = shouldColor(process.stderr);
|
|
142
|
+
process.stderr.write(`${terminalSafe(formatErrorText(error, { color, cwd: options.cwd }))}\n`);
|
|
143
|
+
}
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A DekError whose hint runs as written from `cwd`, about the deck its path is in. */
|
|
148
|
+
function addressError(error: unknown, cwd: string): unknown {
|
|
149
|
+
if (!(error instanceof DekError) || error.path === undefined || error.hint === undefined) {
|
|
150
|
+
return error;
|
|
151
|
+
}
|
|
152
|
+
const hint = addressHint(error.hint, deckAround(error.path), cwd);
|
|
153
|
+
return hint === error.hint
|
|
154
|
+
? error
|
|
155
|
+
: new DekError(error.message, {
|
|
156
|
+
path: error.path,
|
|
157
|
+
hint,
|
|
158
|
+
...(error.line !== undefined ? { line: error.line } : {}),
|
|
159
|
+
cause: error.cause,
|
|
160
|
+
});
|
|
161
|
+
}
|
package/src/cli/scope.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { DekError } from "../core/error.ts";
|
|
4
|
+
import {
|
|
5
|
+
isRefName,
|
|
6
|
+
parseRefSource,
|
|
7
|
+
REF_MARKER,
|
|
8
|
+
readRefMeta,
|
|
9
|
+
refLicense,
|
|
10
|
+
refState,
|
|
11
|
+
} from "../core/ref.ts";
|
|
12
|
+
import {
|
|
13
|
+
locateDeck,
|
|
14
|
+
type Project,
|
|
15
|
+
type ProjectDeck,
|
|
16
|
+
requireSection,
|
|
17
|
+
resolveProject,
|
|
18
|
+
} from "../core/resolve.ts";
|
|
19
|
+
import { suggest } from "../core/suggest.ts";
|
|
20
|
+
|
|
21
|
+
export { requireSection };
|
|
22
|
+
|
|
23
|
+
export type Scope = {
|
|
24
|
+
project: Project;
|
|
25
|
+
deck?: ProjectDeck;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** A deck whose script.md cannot be read, and why. */
|
|
29
|
+
type UnreadableDeck = Project["failed"][number];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What a command works on, which the CLI resolves before it runs: one deck, or every deck in
|
|
33
|
+
* scope (the project's, or the one named).
|
|
34
|
+
*/
|
|
35
|
+
export type DeckScope = "deck" | "decks";
|
|
36
|
+
|
|
37
|
+
/** One deck of a project. */
|
|
38
|
+
export type DeckTarget = { project: Project; deck: ProjectDeck };
|
|
39
|
+
|
|
40
|
+
/** The decks in scope; `deck` is set when one was named or the command runs inside it. */
|
|
41
|
+
export type DecksTarget = {
|
|
42
|
+
project: Project;
|
|
43
|
+
decks: ProjectDeck[];
|
|
44
|
+
deck?: ProjectDeck;
|
|
45
|
+
/**
|
|
46
|
+
* The decks in scope whose script.md cannot be read. Only a command that declares `unreadable`
|
|
47
|
+
* is handed one it named; any other is refused with its error, as before.
|
|
48
|
+
*/
|
|
49
|
+
failed?: UnreadableDeck[];
|
|
50
|
+
/** Set when the deck is a ref. */
|
|
51
|
+
ref?: RefInfo;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function requireProject(cwd: string): Project {
|
|
55
|
+
const project = resolveProject(cwd);
|
|
56
|
+
if (existsSync(join(project.root, REF_MARKER))) {
|
|
57
|
+
const name = readRefMeta(project.root)?.name ?? "<ref>";
|
|
58
|
+
throw new DekError("this directory is inside a ref; refs are read-only", {
|
|
59
|
+
path: project.root,
|
|
60
|
+
hint: `run dek from your own project instead: \`dekc show ${name} <slug>\``,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return project;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function inferDeckName(project: Project, cwd: string): string | undefined {
|
|
67
|
+
const located = locateDeck(project.root, cwd);
|
|
68
|
+
if (!located) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
if (!isKnownDeckName(project, located.name)) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return located.name;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isKnownDeckName(project: Project, name: string): boolean {
|
|
78
|
+
return (
|
|
79
|
+
project.decks.some((deck) => deck.name === name) ||
|
|
80
|
+
project.failed.some((entry) => entry.name === name)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The deck a command's words name first, and the words left for the command. `max` is how many
|
|
86
|
+
* words the command takes without a deck. A ref name is always a deck. More words than the
|
|
87
|
+
* command takes make the first one a deck, found or not, so a typo in it is reported as a deck
|
|
88
|
+
* not found. Otherwise the first word is a deck when it names one of the project's and dek does
|
|
89
|
+
* not run inside a deck, where it names something in that deck instead. A command with
|
|
90
|
+
* subcommands takes a word that is no deck as a mistyped subcommand, never as a deck.
|
|
91
|
+
*/
|
|
92
|
+
export function peelDeckArg(
|
|
93
|
+
cwd: string,
|
|
94
|
+
words: string[],
|
|
95
|
+
grammar: { max: number; subcommands?: boolean },
|
|
96
|
+
): { deck?: string; rest: string[] } {
|
|
97
|
+
const [first, ...rest] = words;
|
|
98
|
+
if (first === undefined) {
|
|
99
|
+
return { rest: words };
|
|
100
|
+
}
|
|
101
|
+
if (isRefName(first)) {
|
|
102
|
+
return { deck: first, rest };
|
|
103
|
+
}
|
|
104
|
+
const extra = words.length > grammar.max;
|
|
105
|
+
if (extra && grammar.subcommands !== true) {
|
|
106
|
+
return { deck: first, rest };
|
|
107
|
+
}
|
|
108
|
+
let project: Project;
|
|
109
|
+
try {
|
|
110
|
+
project = requireProject(cwd);
|
|
111
|
+
} catch {
|
|
112
|
+
return { rest: words };
|
|
113
|
+
}
|
|
114
|
+
if (!isKnownDeckName(project, first)) {
|
|
115
|
+
return { rest: words };
|
|
116
|
+
}
|
|
117
|
+
return extra || inferDeckName(project, cwd) === undefined
|
|
118
|
+
? { deck: first, rest }
|
|
119
|
+
: { rest: words };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Whether a word names a deck of the project dek runs in, or a ref. */
|
|
123
|
+
export function namesDeck(cwd: string, word: string): boolean {
|
|
124
|
+
if (isRefName(word)) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
return isKnownDeckName(requireProject(cwd), word);
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function resolveScope(cwd: string, options: { deck?: string } = {}): Scope {
|
|
135
|
+
const project = requireProject(cwd);
|
|
136
|
+
const name = options.deck ?? inferDeckName(project, cwd);
|
|
137
|
+
if (name === undefined) {
|
|
138
|
+
return { project };
|
|
139
|
+
}
|
|
140
|
+
if (isRefName(name)) {
|
|
141
|
+
throw new DekError(`"${name}" is a ref; refs are read-only`, {
|
|
142
|
+
hint: `read it with \`dekc show ${name} <slug>\`, then copy what you need into your own deck and rewrite it in your theme`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const deck = project.decks.find((entry) => entry.name === name);
|
|
146
|
+
if (!deck) {
|
|
147
|
+
const failed = project.failed.find((entry) => entry.name === name);
|
|
148
|
+
if (failed) {
|
|
149
|
+
throw failed.error;
|
|
150
|
+
}
|
|
151
|
+
const guess = suggest(name, [
|
|
152
|
+
...project.decks.map((entry) => entry.name),
|
|
153
|
+
...project.failed.map((entry) => entry.name),
|
|
154
|
+
]);
|
|
155
|
+
throw new DekError(`deck "${name}" not found`, {
|
|
156
|
+
path: join(project.root, "decks", name),
|
|
157
|
+
hint: guess ? `did you mean \`${guess}\`? run \`dekc ls\` for every deck` : "run `dekc ls`",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return { project, deck };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function resolveDecks(
|
|
164
|
+
cwd: string,
|
|
165
|
+
options: { deck?: string; unreadable?: boolean } = {},
|
|
166
|
+
): DecksTarget {
|
|
167
|
+
if (options.unreadable) {
|
|
168
|
+
const unreadable = unreadableInScope(cwd, options.deck);
|
|
169
|
+
if (unreadable) {
|
|
170
|
+
return { project: unreadable.project, decks: [], failed: [unreadable.failed] };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const scope = resolveScope(cwd, options);
|
|
174
|
+
if (scope.deck) {
|
|
175
|
+
return { project: scope.project, decks: [scope.deck], deck: scope.deck };
|
|
176
|
+
}
|
|
177
|
+
const { failed } = scope.project;
|
|
178
|
+
return {
|
|
179
|
+
project: scope.project,
|
|
180
|
+
decks: scope.project.decks,
|
|
181
|
+
...(failed.length > 0 && { failed }),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The one deck in scope, named or around `cwd`, when its script.md cannot be read. */
|
|
186
|
+
function unreadableInScope(
|
|
187
|
+
cwd: string,
|
|
188
|
+
deck: string | undefined,
|
|
189
|
+
): { project: Project; failed: UnreadableDeck } | undefined {
|
|
190
|
+
const project = requireProject(cwd);
|
|
191
|
+
const name = deck ?? inferDeckName(project, cwd);
|
|
192
|
+
const failed = project.failed.find((entry) => entry.name === name);
|
|
193
|
+
return failed ? { project, failed } : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A deck command run where no deck is in scope. It carries the project's decks, so the CLI, which
|
|
198
|
+
* knows the command line, can hint that line with each one named.
|
|
199
|
+
*/
|
|
200
|
+
export class DeckRequiredError extends DekError {
|
|
201
|
+
readonly decks: ProjectDeck[];
|
|
202
|
+
|
|
203
|
+
constructor(cwd: string, decks: ProjectDeck[]) {
|
|
204
|
+
super("not inside a deck directory; pass a deck name", {
|
|
205
|
+
path: cwd,
|
|
206
|
+
hint: "pass a deck name or --deck <name>",
|
|
207
|
+
});
|
|
208
|
+
this.decks = decks;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function requireDeck(scope: Scope, cwd: string): ProjectDeck {
|
|
213
|
+
if (scope.deck) {
|
|
214
|
+
return scope.deck;
|
|
215
|
+
}
|
|
216
|
+
throw new DeckRequiredError(cwd, scope.project.decks);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function requireDeckFromCwd(cwd: string, deck?: string): DeckTarget {
|
|
220
|
+
const scope = resolveScope(cwd, { deck });
|
|
221
|
+
return { project: scope.project, deck: requireDeck(scope, cwd) };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Where a ref came from, returned by the commands that read one. */
|
|
225
|
+
export type RefInfo = {
|
|
226
|
+
name: string;
|
|
227
|
+
rev: string;
|
|
228
|
+
/** The snapshot directory. */
|
|
229
|
+
dir: string;
|
|
230
|
+
/** The snapshot's license file, or null when the source had none. */
|
|
231
|
+
license: string | null;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/** One deck to read: the project's own, or a ref. */
|
|
235
|
+
export type ReadableDeck = DeckTarget & { ref?: RefInfo };
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* What a command declares it works on, resolved: one deck or the decks in scope. Only a command
|
|
239
|
+
* whose spec says `refs` reads a ref; it never writes, so a ref cannot reach a command that
|
|
240
|
+
* would change it. For every other command resolveScope refuses a ref.
|
|
241
|
+
*/
|
|
242
|
+
export function resolveTarget(cwd: string, scope: "deck", options?: TargetOptions): ReadableDeck;
|
|
243
|
+
export function resolveTarget(cwd: string, scope: "decks", options?: TargetOptions): DecksTarget;
|
|
244
|
+
export function resolveTarget(
|
|
245
|
+
cwd: string,
|
|
246
|
+
scope: DeckScope,
|
|
247
|
+
options?: TargetOptions,
|
|
248
|
+
): ReadableDeck | DecksTarget;
|
|
249
|
+
export function resolveTarget(
|
|
250
|
+
cwd: string,
|
|
251
|
+
scope: DeckScope,
|
|
252
|
+
options: TargetOptions = {},
|
|
253
|
+
): ReadableDeck | DecksTarget {
|
|
254
|
+
const ref =
|
|
255
|
+
options.refs === true && options.deck !== undefined && isRefName(options.deck)
|
|
256
|
+
? resolveRef(cwd, options.deck)
|
|
257
|
+
: undefined;
|
|
258
|
+
if (scope === "deck") {
|
|
259
|
+
return ref ?? requireDeckFromCwd(cwd, options.deck);
|
|
260
|
+
}
|
|
261
|
+
return ref
|
|
262
|
+
? { project: ref.project, decks: [ref.deck], deck: ref.deck, ref: ref.ref }
|
|
263
|
+
: resolveDecks(cwd, { deck: options.deck, unreadable: options.unreadable === true });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** How a command's spec asks for its decks; see `refs` and `unreadable` there. */
|
|
267
|
+
type TargetOptions = { deck?: string; refs?: boolean; unreadable?: boolean };
|
|
268
|
+
|
|
269
|
+
function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
270
|
+
const source = parseRefSource(arg);
|
|
271
|
+
const project = requireProject(cwd);
|
|
272
|
+
const { pinned, dir, fetched } = refState(project, source.name);
|
|
273
|
+
if (pinned === undefined) {
|
|
274
|
+
throw new DekError(`ref "${source.name}" is not added`, {
|
|
275
|
+
path: project.configPath,
|
|
276
|
+
hint: `run \`dekc ref ${arg}\``,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (source.rev !== undefined && source.rev !== pinned) {
|
|
280
|
+
throw new DekError(`"${arg}" is not the pinned version, ${pinned.slice(0, 7)}`, {
|
|
281
|
+
path: project.configPath,
|
|
282
|
+
hint: `run \`dekc ref ${arg}\` to pin that version, or drop @${source.rev}`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (!fetched) {
|
|
286
|
+
throw new DekError(`ref "${source.name}" is not fetched`, {
|
|
287
|
+
path: dir,
|
|
288
|
+
hint: `run \`dekc ref ${source.name}@${pinned}\``,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const snapshot = resolveProject(dir);
|
|
292
|
+
const found = snapshot.decks.find((entry) => entry.name === source.deck);
|
|
293
|
+
if (!found) {
|
|
294
|
+
const failed = snapshot.failed.find((entry) => entry.name === source.deck);
|
|
295
|
+
if (failed) {
|
|
296
|
+
throw failed.error;
|
|
297
|
+
}
|
|
298
|
+
throw new DekError(`ref "${source.name}" has no deck "${source.deck}"`, {
|
|
299
|
+
path: dir,
|
|
300
|
+
hint: `run \`dekc ref ${source.name}@${pinned}\` to fetch it again`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
project: snapshot,
|
|
305
|
+
deck: found,
|
|
306
|
+
ref: { name: source.name, rev: pinned, dir, license: refLicense(dir) },
|
|
307
|
+
};
|
|
308
|
+
}
|
package/src/cli/serve.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { type DevServer, startDevServer } from "../server/dev.ts";
|
|
2
|
+
import { generateRemotePassword, remoteBanner } from "../server/lan.ts";
|
|
3
|
+
import { PAIRING_TTL_MS } from "../server/pairing.ts";
|
|
4
|
+
import { keepDevServer } from "./keep-alive.ts";
|
|
5
|
+
import { qrMatrix, renderQr } from "./qr.ts";
|
|
6
|
+
import { shouldColor } from "./tty.ts";
|
|
7
|
+
|
|
8
|
+
export async function serveCommand(options: {
|
|
9
|
+
cwd: string;
|
|
10
|
+
deck?: string;
|
|
11
|
+
remote?: boolean;
|
|
12
|
+
visual?: boolean;
|
|
13
|
+
port?: number;
|
|
14
|
+
}): Promise<void> {
|
|
15
|
+
await runDevSession(
|
|
16
|
+
{
|
|
17
|
+
cwd: options.cwd,
|
|
18
|
+
deck: options.deck,
|
|
19
|
+
remote: options.remote === true,
|
|
20
|
+
visual: options.visual === true,
|
|
21
|
+
...(options.port !== undefined ? { port: options.port } : {}),
|
|
22
|
+
},
|
|
23
|
+
devSessionView,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type SessionView = { banner: string; pairPath: string };
|
|
28
|
+
|
|
29
|
+
/** What `dekc` prints once up, and which presenter view a phone pairs into. */
|
|
30
|
+
export function devSessionView(
|
|
31
|
+
server: Pick<DevServer, "url" | "remoteUrls" | "deckDir" | "decks">,
|
|
32
|
+
password: string | undefined,
|
|
33
|
+
): SessionView {
|
|
34
|
+
const presenterPaths = server.deckDir
|
|
35
|
+
? ["presenter"]
|
|
36
|
+
: server.decks.map((name) => `decks/${name}/presenter`);
|
|
37
|
+
return {
|
|
38
|
+
banner: devBanner(server.url, server.remoteUrls, { password, presenterPaths }),
|
|
39
|
+
// One deck pairs straight into its presenter view; several, into the list to pick from.
|
|
40
|
+
pairPath: presenterPaths.length === 1 ? (presenterPaths[0] as string) : "",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Start the dev server, print where to open it, offer a phone the presenter view, and serve until
|
|
46
|
+
* stopped. `show` says what to print once the server is up, and which path a QR code opens.
|
|
47
|
+
*/
|
|
48
|
+
export async function runDevSession(
|
|
49
|
+
options: Omit<Parameters<typeof startDevServer>[0], "password">,
|
|
50
|
+
show: (server: DevServer, password: string | undefined) => SessionView,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
const password = options.remote === true ? generateRemotePassword() : undefined;
|
|
53
|
+
const server = await startDevServer({ ...options, password });
|
|
54
|
+
const { banner, pairPath } = show(server, password);
|
|
55
|
+
process.stdout.write(`${banner}\n`);
|
|
56
|
+
offerPairing(server, pairPath);
|
|
57
|
+
await keepDevServer(server);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type Terminal = {
|
|
61
|
+
out: { isTTY?: boolean; write(text: string): unknown };
|
|
62
|
+
input: { isTTY?: boolean; on(event: "data", listener: (chunk: Buffer) => void): unknown };
|
|
63
|
+
color: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* With --remote, a QR code a phone scans to open `path` as the presenter, and a new one each
|
|
68
|
+
* time Enter is pressed, for the next device. Only on a terminal: in a pipe or a log nobody can
|
|
69
|
+
* scan it, and the code in it would only sit there.
|
|
70
|
+
*/
|
|
71
|
+
export function offerPairing(
|
|
72
|
+
server: Pick<DevServer, "remoteUrls" | "pair">,
|
|
73
|
+
path: string,
|
|
74
|
+
terminal: Terminal = {
|
|
75
|
+
out: process.stdout,
|
|
76
|
+
input: process.stdin,
|
|
77
|
+
color: shouldColor(process.stdout),
|
|
78
|
+
},
|
|
79
|
+
): void {
|
|
80
|
+
const pair = server.pair;
|
|
81
|
+
if (!pair || terminal.out.isTTY !== true) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const host = server.remoteUrls.find((url) => !url.includes("127.0.0.1"));
|
|
85
|
+
if (!host) {
|
|
86
|
+
terminal.out.write("\nno LAN address, so no QR code; connect to the network the phone is on\n");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const print = (): void => {
|
|
90
|
+
terminal.out.write(`\n${pairingBlock({ host, path, code: pair(), color: terminal.color })}\n`);
|
|
91
|
+
};
|
|
92
|
+
print();
|
|
93
|
+
if (terminal.input.isTTY === true) {
|
|
94
|
+
terminal.input.on("data", (chunk) => {
|
|
95
|
+
if (/[\r\n]/.test(chunk.toString())) {
|
|
96
|
+
print();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The QR code for one pairing, and what it does; the code is never printed as text. */
|
|
103
|
+
export function pairingBlock(options: {
|
|
104
|
+
host: string;
|
|
105
|
+
path: string;
|
|
106
|
+
code: string;
|
|
107
|
+
color: boolean;
|
|
108
|
+
}): string {
|
|
109
|
+
const url = new URL(options.path, options.host);
|
|
110
|
+
url.searchParams.set("pair", options.code);
|
|
111
|
+
const what = options.path
|
|
112
|
+
? "scan to open the presenter view on a phone"
|
|
113
|
+
: "scan to pick a deck as the presenter on a phone";
|
|
114
|
+
const minutes = PAIRING_TTL_MS / 60_000;
|
|
115
|
+
return `${renderQr(qrMatrix(url.href), { color: options.color })}\n${what} · works once, within ${minutes} minutes · Enter makes a new code`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** What the dev server prints on start: where to open it, then the keys nobody can see. */
|
|
119
|
+
export function devBanner(
|
|
120
|
+
url: string,
|
|
121
|
+
remoteUrls: string[],
|
|
122
|
+
options: Parameters<typeof remoteBanner>[2],
|
|
123
|
+
): string {
|
|
124
|
+
return `${remoteBanner(url, remoteUrls, options)}\n\np presenter view · s slide rail · Ctrl-C stops the server`;
|
|
125
|
+
}
|