@hajimism/dek 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/package.json +8 -7
- package/src/cli/address.ts +59 -15
- package/src/cli/commands.ts +47 -14
- package/src/cli/contract.ts +49 -3
- package/src/cli/files.ts +1 -0
- package/src/cli/format.ts +5 -4
- package/src/cli/goto.ts +41 -10
- package/src/cli/keep-alive.ts +12 -6
- package/src/cli/lint.ts +32 -13
- package/src/cli/ls.ts +5 -1
- package/src/cli/main.ts +48 -13
- package/src/cli/new.ts +14 -1
- package/src/cli/ref.ts +1 -0
- package/src/cli/result.ts +42 -4
- package/src/cli/scope.ts +40 -1
- package/src/cli/shot.ts +77 -6
- package/src/cli/show.ts +15 -2
- package/src/cli/text.ts +61 -9
- package/src/cli/theme.ts +50 -16
- package/src/cli/tty.ts +16 -2
- package/src/cli/voice.ts +11 -6
- package/src/core/assets.ts +18 -10
- package/src/core/attribute-value.ts +188 -0
- package/src/core/build.ts +3 -3
- package/src/core/css-scan.ts +173 -39
- package/src/core/css-transform.ts +57 -11
- package/src/core/css.ts +19 -13
- package/src/core/foreign-text.ts +138 -0
- package/src/core/github.ts +1 -0
- package/src/core/html-entities.ts +318 -0
- package/src/core/html-scan.ts +49 -27
- package/src/core/html.ts +21 -15
- package/src/core/lint/asset-refs.ts +44 -6
- package/src/core/lint/context.ts +2 -0
- package/src/core/lint/shared-styles.ts +26 -1
- package/src/core/lint/slide-html.ts +21 -18
- package/src/core/lint/theme-css.ts +64 -3
- package/src/core/lint.ts +15 -2
- package/src/core/mv.ts +28 -18
- package/src/core/order.ts +14 -0
- package/src/core/path.ts +8 -2
- package/src/core/playwright-module.d.ts +12 -1
- package/src/core/playwright-motion.ts +1 -0
- package/src/core/playwright-sheet.ts +1 -0
- package/src/core/playwright-visual.ts +27 -10
- package/src/core/playwright.ts +77 -19
- package/src/core/pptx.ts +1 -0
- package/src/core/ref-name.ts +8 -3
- package/src/core/ref.ts +145 -47
- package/src/core/resolve.ts +3 -2
- package/src/core/rumdl.ts +51 -25
- package/src/core/safe-fs.ts +19 -2
- package/src/core/sandbox.ts +217 -0
- package/src/core/shot-cache.ts +1 -0
- package/src/core/slide-eval-worker.ts +1 -0
- package/src/core/slide-measure.ts +1 -0
- package/src/core/slide-script-eval.ts +2 -0
- package/src/core/spawn.ts +136 -35
- package/src/core/sync.ts +3 -12
- package/src/core/tar.ts +315 -0
- package/src/core/text-contrast.ts +2 -0
- package/src/core/toml-keys.ts +116 -51
- package/src/core/url-attributes.ts +27 -9
- package/src/core/visual.ts +28 -9
- package/src/core/voice.ts +2 -1
- package/src/server/auth.ts +21 -0
- package/src/server/dev.ts +13 -5
- package/src/server/lock.ts +43 -13
- package/src/server/serial.ts +8 -2
- package/src/video/mux.ts +27 -3
- package/src/video/recorder.ts +3 -2
- package/src/video/worker.ts +1 -0
- package/src/voice/engine.ts +81 -18
- package/tsconfig.json +1 -1
package/src/cli/goto.ts
CHANGED
|
@@ -13,22 +13,42 @@ export type NavResult = {
|
|
|
13
13
|
viewers: number;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
type NavOptions = {
|
|
17
|
+
/** Covers the whole request, from connecting to the last byte of the answer. */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** The dev server answers a nav request at once; a stall this long is a server that is stuck. */
|
|
22
|
+
const NAV_TIMEOUT_MS = 10 * 1000;
|
|
23
|
+
|
|
24
|
+
export async function gotoCommand(
|
|
25
|
+
target: DeckTarget,
|
|
26
|
+
slug: string,
|
|
27
|
+
options: NavOptions = {},
|
|
28
|
+
): Promise<NavResult> {
|
|
17
29
|
requireSection(target.deck, slug);
|
|
18
|
-
return requestDevServer(
|
|
19
|
-
|
|
20
|
-
path: "goto",
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
return requestDevServer(
|
|
31
|
+
target,
|
|
32
|
+
{ method: "POST", path: "goto", body: { slug } },
|
|
33
|
+
options.timeoutMs ?? NAV_TIMEOUT_MS,
|
|
34
|
+
);
|
|
23
35
|
}
|
|
24
36
|
|
|
25
|
-
export async function currentCommand(
|
|
26
|
-
|
|
37
|
+
export async function currentCommand(
|
|
38
|
+
target: DeckTarget,
|
|
39
|
+
options: NavOptions = {},
|
|
40
|
+
): Promise<NavResult> {
|
|
41
|
+
return requestDevServer(
|
|
42
|
+
target,
|
|
43
|
+
{ method: "GET", path: "current" },
|
|
44
|
+
options.timeoutMs ?? NAV_TIMEOUT_MS,
|
|
45
|
+
);
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
async function requestDevServer(
|
|
30
49
|
{ project, deck }: DeckTarget,
|
|
31
50
|
request: { method: "GET" | "POST"; path: "goto" | "current"; body?: { slug: string } },
|
|
51
|
+
timeoutMs: number,
|
|
32
52
|
): Promise<NavResult> {
|
|
33
53
|
const deckName = deck.name;
|
|
34
54
|
const lock = readDevServerLock(project.root);
|
|
@@ -45,16 +65,27 @@ async function requestDevServer(
|
|
|
45
65
|
if (lock.password) {
|
|
46
66
|
headers.authorization = `Basic ${btoa(`:${lock.password}`)}`;
|
|
47
67
|
}
|
|
68
|
+
// A server that takes the connection but never answers is stuck; say so instead of waiting.
|
|
69
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
70
|
+
const unlessStuck = (error: unknown): never => {
|
|
71
|
+
throw signal.aborted
|
|
72
|
+
? new DekError(`the dev server at ${lock.url} did not answer within ${timeoutMs / 1000}s`, {
|
|
73
|
+
hint: "restart it: stop `dekc` and run it again",
|
|
74
|
+
})
|
|
75
|
+
: error;
|
|
76
|
+
};
|
|
77
|
+
// biome-ignore lint/style/noRestrictedGlobals: signal, an AbortSignal.timeout, bounds the request and the read of its answer.
|
|
48
78
|
const response = await fetch(url, {
|
|
49
79
|
method: request.method,
|
|
50
80
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
51
81
|
body: request.body ? JSON.stringify(request.body) : undefined,
|
|
52
|
-
|
|
82
|
+
signal,
|
|
83
|
+
}).catch(unlessStuck);
|
|
53
84
|
// Only the nav routes answer JSON; a 404, 401, or 405 from the server itself is plain text.
|
|
54
85
|
if (!response.headers.get("content-type")?.includes("application/json")) {
|
|
55
86
|
throw refusal(response.status, lock, deckName, request.path);
|
|
56
87
|
}
|
|
57
|
-
const json = (await response.json()) as {
|
|
88
|
+
const json = (await response.json().catch(unlessStuck)) as {
|
|
58
89
|
ok?: boolean;
|
|
59
90
|
slug?: string;
|
|
60
91
|
slideIndex?: number;
|
package/src/cli/keep-alive.ts
CHANGED
|
@@ -7,6 +7,13 @@ const PARENT_POLL_MS = 1_000;
|
|
|
7
7
|
/** How long a stop waits for the server to close before the process exits anyway. */
|
|
8
8
|
const SHUTDOWN_GRACE_MS = 2_000;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The process that started this one, read as the CLI loads. A parent read once the server is up
|
|
12
|
+
* would come too late: whoever waits for the URL can be killed before the next line runs, and the
|
|
13
|
+
* parent read then is already init, which never goes away.
|
|
14
|
+
*/
|
|
15
|
+
const startedBy = process.ppid;
|
|
16
|
+
|
|
10
17
|
/**
|
|
11
18
|
* Serve until told to stop: Ctrl-C, a kill, a closed terminal, or the process that started the
|
|
12
19
|
* server going away, so a test run or an editor that dies without cleaning up leaves no server
|
|
@@ -51,16 +58,15 @@ export function stopOnce(
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/**
|
|
54
|
-
* Call `fn` once this process is
|
|
55
|
-
* leaves it no chance to stop its children. A process started
|
|
56
|
-
* (already under init) is never orphaned.
|
|
61
|
+
* Call `fn` once this process is no longer under `parent`, the one that started it, which is how
|
|
62
|
+
* Unix says the parent died: a SIGKILL leaves it no chance to stop its children. A process started
|
|
63
|
+
* with no parent of its own to lose (already under init) is never orphaned.
|
|
57
64
|
*/
|
|
58
65
|
export function onOrphaned(
|
|
59
66
|
fn: () => void,
|
|
60
|
-
options: { ppid?: () => number; intervalMs?: number } = {},
|
|
67
|
+
options: { parent?: number; ppid?: () => number; intervalMs?: number } = {},
|
|
61
68
|
): () => void {
|
|
62
|
-
const
|
|
63
|
-
const parent = ppid();
|
|
69
|
+
const { parent = startedBy, ppid = () => process.ppid } = options;
|
|
64
70
|
const timer = setInterval(() => {
|
|
65
71
|
if (ppid() !== parent) {
|
|
66
72
|
clearInterval(timer);
|
package/src/cli/lint.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Diagnostic } from "../core/diagnostic.ts";
|
|
|
4
4
|
import { shellQuote } from "../core/escape.ts";
|
|
5
5
|
import { lintDeck, lintProject, unreadableScriptDiagnostics } from "../core/lint.ts";
|
|
6
6
|
import { playwrightMissingError } from "../core/playwright.ts";
|
|
7
|
-
import { resolveRumdlBin, runRumdl } from "../core/rumdl.ts";
|
|
7
|
+
import { type RumdlRunner, type RumdlSkip, resolveRumdlBin, runRumdl } from "../core/rumdl.ts";
|
|
8
8
|
import { mergeSarif, type SarifLog } from "../core/sarif.ts";
|
|
9
9
|
import { syncDeck } from "../core/sync.ts";
|
|
10
10
|
import { lintVisualDeck } from "../core/visual.ts";
|
|
@@ -18,10 +18,10 @@ export type LintCliResult = {
|
|
|
18
18
|
rumdlSarif?: SarifLog;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
/** `cwd` is where a skipped rumdl's hint says to run it from. */
|
|
21
|
+
/** `cwd` is where a skipped rumdl's hint says to run it from; `rumdl` runs it, dek's by default. */
|
|
22
22
|
export async function lintCommand(
|
|
23
23
|
{ project, decks, failed = [] }: DecksTarget,
|
|
24
|
-
options: { cwd: string; fix?: boolean; visual?: boolean },
|
|
24
|
+
options: { cwd: string; fix?: boolean; visual?: boolean; rumdl?: RumdlRunner },
|
|
25
25
|
): Promise<LintCliResult> {
|
|
26
26
|
// dek.toml's findings once, then each deck's.
|
|
27
27
|
const diagnostics: Diagnostic[] = [
|
|
@@ -30,19 +30,25 @@ export async function lintCommand(
|
|
|
30
30
|
];
|
|
31
31
|
let rumdlSkip: SkippedCheck | undefined;
|
|
32
32
|
let rumdlSarif: SarifLog | undefined;
|
|
33
|
+
// A rumdl that hung on one deck would hang on each, half a minute apiece.
|
|
34
|
+
let rumdlHung = false;
|
|
33
35
|
|
|
34
36
|
for (const deck of decks) {
|
|
35
37
|
if (options.fix) {
|
|
36
38
|
syncDeck({ project, deck });
|
|
37
39
|
}
|
|
38
40
|
diagnostics.push(...lintDeck({ project, deck }));
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
if (!rumdlHung) {
|
|
42
|
+
const markdown = await runRumdl(deck.scriptPath, options.rumdl);
|
|
43
|
+
diagnostics.push(...markdown.diagnostics);
|
|
44
|
+
// One entry for the run: the first reason, unless rumdl hung, which covers every deck after.
|
|
45
|
+
if (markdown.skipped && (!rumdlSkip || markdown.skipped.hung)) {
|
|
46
|
+
rumdlSkip = rumdlSkipped(deck.scriptPath, options.cwd, markdown.skipped);
|
|
47
|
+
}
|
|
48
|
+
rumdlHung = markdown.skipped?.hung === true;
|
|
49
|
+
if (markdown.sarif) {
|
|
50
|
+
rumdlSarif = rumdlSarif ? mergeSarif(rumdlSarif, markdown.sarif) : markdown.sarif;
|
|
51
|
+
}
|
|
46
52
|
}
|
|
47
53
|
if (options.visual) {
|
|
48
54
|
const visual = await lintVisualDeck({ project, deck });
|
|
@@ -71,15 +77,28 @@ export async function lintCommand(
|
|
|
71
77
|
|
|
72
78
|
const RUMDL_INSTALL = "bun add -d rumdl; or put rumdl on PATH, or set DEK_RUMDL to its path";
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
const RUMDL_HUNG =
|
|
81
|
+
"update or reinstall rumdl (`bun add -d rumdl@latest`), or set DEK_RUMDL to a rumdl that finishes; lint skips it for every deck after one it hangs on";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Why rumdl gave no result: whether dek found it, on PATH, in node_modules/.bin, or at DEK_RUMDL,
|
|
85
|
+
* and the reason when dek knows it. One that hung would hang again, so its hint is not to rerun.
|
|
86
|
+
*/
|
|
87
|
+
function rumdlSkipped(scriptPath: string, cwd: string, skip: RumdlSkip): SkippedCheck {
|
|
76
88
|
const bin = resolveRumdlBin();
|
|
77
89
|
if (bin === undefined || !existsSync(bin)) {
|
|
78
90
|
return { check: "rumdl", reason: "rumdl is not installed", hint: RUMDL_INSTALL };
|
|
79
91
|
}
|
|
92
|
+
if (skip.hung) {
|
|
93
|
+
return {
|
|
94
|
+
check: "rumdl",
|
|
95
|
+
reason: skip.reason ?? "rumdl hung and was stopped",
|
|
96
|
+
hint: RUMDL_HUNG,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
80
99
|
return {
|
|
81
100
|
check: "rumdl",
|
|
82
|
-
reason: "rumdl ran but gave no result",
|
|
101
|
+
reason: skip.reason ?? "rumdl ran but gave no result",
|
|
83
102
|
hint: `run \`${shellQuote(bin)} check ${shellQuote(relative(cwd, scriptPath) || scriptPath)}\` to see why`,
|
|
84
103
|
};
|
|
85
104
|
}
|
package/src/cli/ls.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Diagnostic } from "../core/diagnostic.ts";
|
|
2
|
+
import type { HiddenText } from "../core/foreign-text.ts";
|
|
2
3
|
import { lintDeck, lintProject } from "../core/lint.ts";
|
|
3
4
|
import { listSlides, type Project, type ProjectDeck } from "../core/resolve.ts";
|
|
4
5
|
import { slideVideoSeconds, timelineSeconds } from "../core/timeline.ts";
|
|
5
6
|
import { sectionTiming } from "../core/timing.ts";
|
|
6
7
|
import { tryLoadCachedTimeline } from "../core/voice.ts";
|
|
7
8
|
import { type SkippedCheck, skippedChecks } from "./result.ts";
|
|
8
|
-
import type
|
|
9
|
+
import { type DecksTarget, hiddenField, hiddenInFiles, type RefInfo } from "./scope.ts";
|
|
9
10
|
|
|
10
11
|
export type LsListResult = {
|
|
11
12
|
kind: "list";
|
|
@@ -46,6 +47,8 @@ export type LsDeckResult = {
|
|
|
46
47
|
skipped?: SkippedCheck[];
|
|
47
48
|
/** Set when the deck is a ref. */
|
|
48
49
|
ref?: RefInfo;
|
|
50
|
+
/** A ref's lines of script.md that hold characters a person cannot see. */
|
|
51
|
+
hidden?: HiddenText[];
|
|
49
52
|
};
|
|
50
53
|
|
|
51
54
|
/** The one deck in scope, a ref included, or else every deck of the project. */
|
|
@@ -58,6 +61,7 @@ export function lsCommand(target: DecksTarget): LsListResult | LsDeckResult {
|
|
|
58
61
|
{ check: "lint", reason: "a ref is read-only; its problems are not yours to fix" },
|
|
59
62
|
]),
|
|
60
63
|
ref,
|
|
64
|
+
...hiddenField(ref, hiddenInFiles(deck.dir, [deck.scriptPath])),
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
if (deck) {
|
package/src/cli/main.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { parseCss } from "../core/css.ts";
|
|
2
|
+
import { deckPaths } from "../core/deck-paths.ts";
|
|
1
3
|
import { DekError } from "../core/error.ts";
|
|
2
4
|
import { shellQuote } from "../core/escape.ts";
|
|
3
5
|
import { either } from "../core/prose.ts";
|
|
4
6
|
import { isRefName } from "../core/ref-name.ts";
|
|
5
|
-
import { resolveProject } from "../core/resolve.ts";
|
|
6
|
-
import {
|
|
7
|
+
import { type ProjectDeck, readDeckFile, resolveProject } from "../core/resolve.ts";
|
|
8
|
+
import { takeUnsandboxed } from "../core/sandbox.ts";
|
|
9
|
+
import { themeFacts } from "../core/theme-facts.ts";
|
|
10
|
+
import { addressCommands, deckWord, type HintDeck, withPlaceholderNote } from "./address.ts";
|
|
7
11
|
import {
|
|
8
12
|
type AnySpec,
|
|
9
13
|
type CliResult,
|
|
@@ -158,7 +162,7 @@ async function run(cwd: string, line: CommandLine): Promise<void> {
|
|
|
158
162
|
}
|
|
159
163
|
const scope = spec.scope;
|
|
160
164
|
const target = scope
|
|
161
|
-
? retypedWithDeck(line, cwd, () =>
|
|
165
|
+
? retypedWithDeck(line, cwd, call, () =>
|
|
162
166
|
resolveTarget(cwd, scope, {
|
|
163
167
|
deck: call.deck,
|
|
164
168
|
refs: spec.refs === true,
|
|
@@ -170,10 +174,13 @@ async function run(cwd: string, line: CommandLine): Promise<void> {
|
|
|
170
174
|
const ctx = { cwd, flags: line.values, args: call.args, target };
|
|
171
175
|
const data = await form.run?.(ctx);
|
|
172
176
|
if (spec.kind === "result") {
|
|
173
|
-
|
|
177
|
+
// Said on stderr as Chromium drew without its sandbox, and kept here for --json.
|
|
178
|
+
const drawn = { ...(data as object), ...takeUnsandboxed() };
|
|
179
|
+
writeSuccess({ command: call.name, data: drawn } as CliResult, {
|
|
174
180
|
json: line.values.json === true,
|
|
175
181
|
format: line.values.format,
|
|
176
182
|
cwd,
|
|
183
|
+
flags: line.values,
|
|
177
184
|
...(target?.deck && {
|
|
178
185
|
deck: { name: target.ref?.name ?? target.deck.name, dir: target.deck.dir },
|
|
179
186
|
}),
|
|
@@ -186,9 +193,10 @@ const NAMED_DECKS = 3;
|
|
|
186
193
|
|
|
187
194
|
/**
|
|
188
195
|
* `resolve`, with a missing deck hinted as the line that was typed, the deck named where the
|
|
189
|
-
* command takes it: one command per deck of the project
|
|
196
|
+
* command takes it: one command per deck of the project that has what the line names, so the
|
|
197
|
+
* hint runs as written.
|
|
190
198
|
*/
|
|
191
|
-
function retypedWithDeck<T>(line: CommandLine, cwd: string, resolve: () => T): T {
|
|
199
|
+
function retypedWithDeck<T>(line: CommandLine, cwd: string, call: Call, resolve: () => T): T {
|
|
192
200
|
try {
|
|
193
201
|
return resolve();
|
|
194
202
|
} catch (error) {
|
|
@@ -196,18 +204,43 @@ function retypedWithDeck<T>(line: CommandLine, cwd: string, resolve: () => T): T
|
|
|
196
204
|
throw error;
|
|
197
205
|
}
|
|
198
206
|
const typed = `\`dekc ${line.typed.map(shellQuote).join(" ")}\``;
|
|
199
|
-
const
|
|
207
|
+
const decks = error.decks.filter((deck) => runsIn(call, deck));
|
|
208
|
+
const [first] = decks;
|
|
200
209
|
// Each command is retyped for one deck; where to find a name left as <deck> is said once.
|
|
201
|
-
const retyped = (deck: HintDeck) =>
|
|
202
|
-
const hint =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
const retyped = (deck: HintDeck) => addressCommands(typed, deck, cwd, { note: false });
|
|
211
|
+
const hint =
|
|
212
|
+
error.decks.length === 0
|
|
213
|
+
? "run `dekc new <name>` to make a deck"
|
|
214
|
+
: !first
|
|
215
|
+
? "run `dekc ls`"
|
|
216
|
+
: decks.length > NAMED_DECKS
|
|
217
|
+
? `run ${retyped(first)}, or name another deck in place of ${deckWord(first.name)}; \`dekc ls\` lists them`
|
|
218
|
+
: `run ${either(decks.map(retyped))}`;
|
|
207
219
|
throw new DekError(error.message, { hint: withPlaceholderNote(hint), cause: error });
|
|
208
220
|
}
|
|
209
221
|
}
|
|
210
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Whether `call` finds in `deck` what it names: a section, the one `dekc mv` renames or the name
|
|
225
|
+
* script.md already gives it, and a layout of the deck's theme.
|
|
226
|
+
*/
|
|
227
|
+
function runsIn(call: Call, deck: ProjectDeck): boolean {
|
|
228
|
+
const { slug, old, layout } = call.args;
|
|
229
|
+
const has = (name: string | undefined) =>
|
|
230
|
+
deck.deck.sections.some((section) => section.slug === name);
|
|
231
|
+
return (
|
|
232
|
+
(slug === undefined || has(slug)) &&
|
|
233
|
+
(old === undefined || has(old) || has(call.args.new)) &&
|
|
234
|
+
(layout === undefined || themeLayouts(deck).includes(layout))
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The layouts a deck's theme defines; none when it has no theme.css. */
|
|
239
|
+
function themeLayouts(deck: ProjectDeck): string[] {
|
|
240
|
+
const css = readDeckFile(deck.dir, deckPaths(deck.dir).theme) ?? "";
|
|
241
|
+
return themeFacts(parseCss(css)).layouts.map((entry) => entry.name);
|
|
242
|
+
}
|
|
243
|
+
|
|
211
244
|
/** A session prints no result, so --json has nothing to shape, and a reader would wait forever. */
|
|
212
245
|
function sessionJsonError(name: string): DekError {
|
|
213
246
|
return new DekError(`${name} keeps running until stopped and prints no JSON`, {
|
|
@@ -220,6 +253,8 @@ function sessionJsonError(name: string): DekError {
|
|
|
220
253
|
* prints once, here: as JSON when the line asks for it, even a line too broken to parse.
|
|
221
254
|
*/
|
|
222
255
|
export async function main(argv: string[], cwd: string): Promise<void> {
|
|
256
|
+
// What a run before this one in the process drew is its own, even one that failed.
|
|
257
|
+
takeUnsandboxed();
|
|
223
258
|
let json = wantsJson(argv);
|
|
224
259
|
try {
|
|
225
260
|
const line = parseCommandLine(argv);
|
package/src/cli/new.ts
CHANGED
|
@@ -3,13 +3,20 @@ import { join } from "node:path";
|
|
|
3
3
|
import { loadConfig } from "../core/config.ts";
|
|
4
4
|
import { deckPaths } from "../core/deck-paths.ts";
|
|
5
5
|
import { DekError } from "../core/error.ts";
|
|
6
|
+
import { type HiddenText, hiddenText } from "../core/foreign-text.ts";
|
|
6
7
|
import { DECK_NAME_HINT, isDeckName } from "../core/path.ts";
|
|
7
8
|
import { isRefName } from "../core/ref-name.ts";
|
|
8
9
|
import { readDeckFile } from "../core/resolve.ts";
|
|
9
10
|
import { readSourceIfExists } from "../core/safe-fs.ts";
|
|
10
11
|
import { syncDeck } from "../core/sync.ts";
|
|
11
12
|
import { applyPlan, deckPlan, nextSteps } from "./files.ts";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
hiddenField,
|
|
15
|
+
type RefInfo,
|
|
16
|
+
requireDeckFromCwd,
|
|
17
|
+
requireProject,
|
|
18
|
+
resolveTarget,
|
|
19
|
+
} from "./scope.ts";
|
|
13
20
|
|
|
14
21
|
export type NewResult = {
|
|
15
22
|
name: string;
|
|
@@ -21,6 +28,11 @@ export type NewResult = {
|
|
|
21
28
|
next: string[];
|
|
22
29
|
/** Set when the theme came from a ref with no license. */
|
|
23
30
|
warnings: string[];
|
|
31
|
+
/**
|
|
32
|
+
* A ref's theme's lines, now the new deck's theme.css, that hold characters a person cannot
|
|
33
|
+
* see: the copy is the deck's own, so they are its author's to remove.
|
|
34
|
+
*/
|
|
35
|
+
hidden?: HiddenText[];
|
|
24
36
|
};
|
|
25
37
|
|
|
26
38
|
export function newCommand(options: { cwd: string; name: string; themeFrom?: string }): NewResult {
|
|
@@ -87,6 +99,7 @@ export function newCommand(options: { cwd: string; name: string; themeFrom?: str
|
|
|
87
99
|
`${ref.name} has no license; ask its author before you present or share a deck built on its theme`,
|
|
88
100
|
]
|
|
89
101
|
: [],
|
|
102
|
+
...hiddenField(ref, ref ? hiddenText(theme, deckPaths(dir).theme) : []),
|
|
90
103
|
};
|
|
91
104
|
}
|
|
92
105
|
|
package/src/cli/ref.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// biome-ignore lint/style/noRestrictedImports: rmdirSync, which takes packs/ and refs/ folders only while empty; removeInside would take what is in them.
|
|
1
2
|
import { existsSync, lstatSync, readdirSync, readFileSync, rmdirSync, statSync } from "node:fs";
|
|
2
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
4
|
import { loadConfig } from "../core/config.ts";
|
package/src/cli/result.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Diagnostic, SkippedCheck } from "../core/diagnostic.ts";
|
|
2
2
|
import { DekError } from "../core/error.ts";
|
|
3
|
+
import { revealHidden } from "../core/foreign-text.ts";
|
|
3
4
|
import { PLAYWRIGHT_INSTALL, type PlaywrightRunner, playwrightReady } from "../core/playwright.ts";
|
|
4
5
|
import { addressHint, deckAround, type HintDeck } from "./address.ts";
|
|
5
6
|
import { type CliResult, outputOf } from "./commands.ts";
|
|
7
|
+
import type { FlagValues } from "./flags.ts";
|
|
6
8
|
import { displayPath, formatError, formatErrorText } from "./format.ts";
|
|
7
|
-
import { shouldColor, terminalSafe } from "./tty.ts";
|
|
9
|
+
import { plainText, shouldColor, terminalSafe } from "./tty.ts";
|
|
8
10
|
|
|
9
11
|
export type { CliResult, SkippedCheck };
|
|
10
12
|
|
|
@@ -41,6 +43,8 @@ export type WriteSuccessOptions = {
|
|
|
41
43
|
cwd?: string;
|
|
42
44
|
/** The one deck the command worked on, which hints that name no path are about. */
|
|
43
45
|
deck?: HintDeck;
|
|
46
|
+
/** The flags it ran with, which a failure's rerun repeats where they change what is checked. */
|
|
47
|
+
flags?: FlagValues;
|
|
44
48
|
};
|
|
45
49
|
|
|
46
50
|
/** Where a run's hints are read: the directory it ran in, and the deck it worked on, if one. */
|
|
@@ -86,7 +90,7 @@ export function writeSuccess(original: CliResult, options: WriteSuccessOptions):
|
|
|
86
90
|
const { cwd } = options;
|
|
87
91
|
const addressed = cwd === undefined ? original : addressResult(original, { cwd, ...options });
|
|
88
92
|
const result = cwd === undefined ? addressed : displayPaths(addressed, cwd);
|
|
89
|
-
const failed = output.failure?.(result.data);
|
|
93
|
+
const failed = output.failure?.(result.data, options.flags ?? {});
|
|
90
94
|
const failure =
|
|
91
95
|
failed?.hint === undefined || cwd === undefined
|
|
92
96
|
? failed
|
|
@@ -98,9 +102,13 @@ export function writeSuccess(original: CliResult, options: WriteSuccessOptions):
|
|
|
98
102
|
const data = output.json ? output.json(result.data) : (result.data as object);
|
|
99
103
|
process.stdout.write(`${JSON.stringify({ ...envelope, ...data })}\n`);
|
|
100
104
|
} else {
|
|
101
|
-
const
|
|
105
|
+
const plain = {
|
|
106
|
+
...result,
|
|
107
|
+
data: plainStrings(result.data, isAboutRef(result.data)),
|
|
108
|
+
} as CliResult;
|
|
109
|
+
const text = formatText(plain, shouldColor(process.stdout));
|
|
102
110
|
process.stdout.write(`${terminalSafe(text)}\n`);
|
|
103
|
-
const notes = output.notes?.(
|
|
111
|
+
const notes = output.notes?.(plain.data, shouldColor(process.stderr));
|
|
104
112
|
if (notes) {
|
|
105
113
|
process.stderr.write(`${terminalSafe(notes)}\n`);
|
|
106
114
|
}
|
|
@@ -126,6 +134,36 @@ export function displayPaths(result: CliResult, cwd: string): CliResult {
|
|
|
126
134
|
return { ...result, data: paths(result.data, (path) => displayPath(path, cwd)) } as CliResult;
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
/** A result that read a ref, as `ls`, `show`, and `theme` mark theirs. */
|
|
138
|
+
function isAboutRef(data: unknown): boolean {
|
|
139
|
+
return typeof data === "object" && data !== null && "ref" in data && data.ref !== undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Every string in a result as plainText, so the text a deck or a ref wrote reaches the terminal
|
|
144
|
+
* with no escape of its own; the text formatters paint dek's colors after. In a result about a
|
|
145
|
+
* ref, each character a person cannot see shows as `⟨U+XXXX⟩` too.
|
|
146
|
+
*/
|
|
147
|
+
function plainStrings<T>(value: T, reveal: boolean): T {
|
|
148
|
+
if (typeof value === "string") {
|
|
149
|
+
const plain = plainText(value);
|
|
150
|
+
return (reveal ? revealHidden(plain) : plain) as T;
|
|
151
|
+
}
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
return value.map((entry) => plainStrings(entry, reveal)) as T;
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
value !== null &&
|
|
157
|
+
typeof value === "object" &&
|
|
158
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
159
|
+
) {
|
|
160
|
+
return Object.fromEntries(
|
|
161
|
+
Object.entries(value).map(([key, entry]) => [key, plainStrings(entry, reveal)]),
|
|
162
|
+
) as T;
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
129
167
|
/** A result as text, without what did not run; see writeSuccess. */
|
|
130
168
|
export function formatText(result: CliResult, color = false): string {
|
|
131
169
|
return outputOf(result.command).text(result.data, color);
|
package/src/cli/scope.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { join, relative } from "node:path";
|
|
2
2
|
import { DekError } from "../core/error.ts";
|
|
3
3
|
import { shellName, shellQuote } from "../core/escape.ts";
|
|
4
|
+
import { foreignLine, type HiddenText, hiddenText, MESSAGE_MAX } from "../core/foreign-text.ts";
|
|
4
5
|
import {
|
|
5
6
|
isRefName,
|
|
6
7
|
packFile,
|
|
7
8
|
parseRefSource,
|
|
9
|
+
type RefSource,
|
|
8
10
|
refLicense,
|
|
9
11
|
refReadOnlyError,
|
|
10
12
|
refState,
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
locateDeck,
|
|
15
17
|
type Project,
|
|
16
18
|
type ProjectDeck,
|
|
19
|
+
readDeckFile,
|
|
17
20
|
requireSection,
|
|
18
21
|
resolveProject,
|
|
19
22
|
} from "../core/resolve.ts";
|
|
@@ -222,6 +225,25 @@ export type RefInfo = {
|
|
|
222
225
|
/** One deck to read: the project's own, or a ref. */
|
|
223
226
|
export type ReadableDeck = DeckTarget & { ref?: RefInfo };
|
|
224
227
|
|
|
228
|
+
/**
|
|
229
|
+
* `{ hidden }` when a ref's files hold characters a person cannot see, nothing otherwise. Only a
|
|
230
|
+
* ref's are reported: a deck of the project's own is its author's, emoji joiners and all.
|
|
231
|
+
*/
|
|
232
|
+
export function hiddenField(
|
|
233
|
+
ref: RefInfo | undefined,
|
|
234
|
+
found: HiddenText[],
|
|
235
|
+
): { hidden?: HiddenText[] } {
|
|
236
|
+
return ref && found.length > 0 ? { hidden: found } : {};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** What the files at `paths` in the deck hold that a person cannot see, file by file. */
|
|
240
|
+
export function hiddenInFiles(deckDir: string, paths: string[]): HiddenText[] {
|
|
241
|
+
return paths.flatMap((path) => {
|
|
242
|
+
const text = readDeckFile(deckDir, path);
|
|
243
|
+
return text === undefined ? [] : hiddenText(text, path);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
225
247
|
/**
|
|
226
248
|
* What a command declares it works on, resolved: one deck or the decks in scope. Only a command
|
|
227
249
|
* whose spec says `refs` reads a ref; it never writes, so a ref cannot reach a command that
|
|
@@ -287,7 +309,7 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
287
309
|
if (!found) {
|
|
288
310
|
const failed = snapshot.failed.find((entry) => entry.name === source.deck);
|
|
289
311
|
if (failed) {
|
|
290
|
-
throw failed.error;
|
|
312
|
+
throw unreadableRef(failed.error, source);
|
|
291
313
|
}
|
|
292
314
|
throw new DekError(`ref "${source.name}" has no deck "${source.deck}"`, {
|
|
293
315
|
path: dir,
|
|
@@ -304,3 +326,20 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
304
326
|
ref: { name: source.name, rev: pinned, dir, license: refLicense(dir) },
|
|
305
327
|
};
|
|
306
328
|
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* A ref whose script.md cannot be read. The error's hint would say how to fix the file, quoting
|
|
332
|
+
* it, and a ref is not the reader's to fix: the message stays, on one line, and the hint is dek's.
|
|
333
|
+
*/
|
|
334
|
+
function unreadableRef(error: Error, source: RefSource): DekError {
|
|
335
|
+
const fields = error instanceof DekError ? error : undefined;
|
|
336
|
+
return new DekError(foreignLine(error.message, MESSAGE_MAX), {
|
|
337
|
+
...(fields?.path !== undefined ? { path: fields.path } : {}),
|
|
338
|
+
...(fields?.line !== undefined ? { line: fields.line } : {}),
|
|
339
|
+
hint:
|
|
340
|
+
source.kind === "file"
|
|
341
|
+
? "a ref is read-only, so its script.md is its author's to fix; ask them for a pack that reads, and add it with `dekc ref <file>.tgz`"
|
|
342
|
+
: `a ref is read-only, so its script.md is its author's to fix; pin a commit where it reads with \`dekc ref ${source.name}@<rev>\``,
|
|
343
|
+
cause: error,
|
|
344
|
+
});
|
|
345
|
+
}
|