@hajimism/dek 0.3.1 → 0.3.3
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 +46 -0
- package/package.json +5 -5
- package/src/cli/commands.ts +45 -8
- package/src/cli/contract.ts +9 -0
- package/src/cli/files.ts +8 -1
- package/src/cli/new.ts +32 -8
- package/src/cli/pack.ts +21 -0
- package/src/cli/ref.ts +156 -4
- package/src/cli/scope.ts +14 -5
- package/src/cli/text.ts +18 -2
- package/src/core/config-keys.ts +6 -2
- package/src/core/config.ts +32 -5
- package/src/core/licenses/CC-BY-4.0.txt +396 -0
- package/src/core/licenses/CC0-1.0.txt +121 -0
- package/src/core/licenses.ts +20 -0
- package/src/core/pack.ts +137 -0
- package/src/core/pdf-text.ts +191 -0
- package/src/core/playwright-module.d.ts +6 -1
- package/src/core/playwright-visual.ts +40 -4
- package/src/core/playwright.ts +4 -1
- package/src/core/ref-name.ts +56 -12
- package/src/core/ref.ts +148 -20
- package/src/core/sync.ts +1 -1
- package/src/runtime/annotate-view.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
Release notes for `@hajimism/dek`. From 0.1.0 on, release-please writes this file from the Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [0.3.3](https://github.com/hajimism/dek/compare/v0.3.2...v0.3.3) (2026-10-21)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **new:** start a deck on a ref's theme with `--theme-from <ref>` ([6d0ffc9](https://github.com/hajimism/dek/commit/6d0ffc9c45a54092375ca73e305294e71e005655))
|
|
11
|
+
* **new:** start a deck on a ref's theme with `--theme-from <ref>` ([54be6db](https://github.com/hajimism/dek/commit/54be6db71b2521fda1bec3ddb967a4a7545bc36c))
|
|
12
|
+
* **pack:** hand a deck over under CC0 1.0 unless dek.toml says otherwise ([b7e2565](https://github.com/hajimism/dek/commit/b7e2565fd83a8d82c8d8416f6ae3551ba5040427))
|
|
13
|
+
* **pack:** hand a deck over under CC0 1.0 unless dek.toml says otherwise ([46cd7b9](https://github.com/hajimism/dek/commit/46cd7b9424d9e76570c2e017f2345b41f85506b3))
|
|
14
|
+
* **ref:** hand a deck over with `dekc pack` and read it with `dekc ref <file>.tgz` ([9e77bbc](https://github.com/hajimism/dek/commit/9e77bbc7bdf9f2e232c115903c73bf8fa8f078fe))
|
|
15
|
+
* **ref:** hand a deck over with `dekc pack` and read it with `dekc ref <file>.tgz` ([a0c67bc](https://github.com/hajimism/dek/commit/a0c67bc8258a2132dd77eadb2b87c838e6e9f866))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **dev:** leave the Enter and Escape that end a conversion to the input method ([f148c54](https://github.com/hajimism/dek/commit/f148c54377460b4df11e8fedd059554f00c06889))
|
|
21
|
+
* **dev:** leave the Enter and Escape that end a conversion to the input method ([e2b3940](https://github.com/hajimism/dek/commit/e2b3940cf48168b5a7c2b95250df982566222b60))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
* **adr:** hand a deck over as a pack and read it as a ref ([4b3ffc6](https://github.com/hajimism/dek/commit/4b3ffc6e653757f3976a581acff1a2bf76862748))
|
|
27
|
+
* **adr:** start a deck from a ref's theme ([f836941](https://github.com/hajimism/dek/commit/f836941cb2faf9b33faaab63abd16f66878e4767))
|
|
28
|
+
* describe packs in the structure guide, the reference, and ADR 15 ([020d4a0](https://github.com/hajimism/dek/commit/020d4a0254138c55e0a78c41e2e28f8a839bf8d1))
|
|
29
|
+
* record the pack's license in ADR 16, the guide, and the reference ([a0b4ea7](https://github.com/hajimism/dek/commit/a0b4ea74b6d896d102c1c0e2c06a040b75905962))
|
|
30
|
+
* say a deck can start on a ref's theme, in the guides and the reference ([49791ff](https://github.com/hajimism/dek/commit/49791ff46f77e5fd37e9d21f4061669de31af1f9))
|
|
31
|
+
|
|
32
|
+
## [0.3.2](https://github.com/hajimism/dek/compare/v0.3.1...v0.3.2) (2026-10-20)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Bug Fixes
|
|
36
|
+
|
|
37
|
+
* **pdf:** print each slide as a picture with its text laid over it ([02f49a8](https://github.com/hajimism/dek/commit/02f49a855209e1c4430613fd9358699305bcbb9b))
|
|
38
|
+
* **pdf:** print each slide as a picture with its text laid over it ([b88259d](https://github.com/hajimism/dek/commit/b88259dbcc80ed0a1450cc6bf6422c71120c5802))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### Performance
|
|
42
|
+
|
|
43
|
+
* **pdf:** keep a slide of photographs as a JPEG ([19a9092](https://github.com/hajimism/dek/commit/19a909214b256c8bbcf66a443f00c54c2b43766e))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Documentation
|
|
47
|
+
|
|
48
|
+
* record the PDF as pictures in ADR 14 and the presenting guide ([c889781](https://github.com/hajimism/dek/commit/c88978175a9b3812f611d8af4de94b2de57556c1))
|
|
49
|
+
* say which slides the PDF keeps as JPEGs, in ADR 14 and the guide ([de733fb](https://github.com/hajimism/dek/commit/de733fb4589d5f6f45f6f9a83b60673d2feab706))
|
|
50
|
+
|
|
5
51
|
## [0.3.1](https://github.com/hajimism/dek/compare/v0.3.0...v0.3.1) (2026-10-08)
|
|
6
52
|
|
|
7
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hajimism/dek",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "A build system for talks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hajime Nakagawa (https://github.com/hajimism)",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
"zod": "^4.6.5"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@biomejs/biome": "2.5.
|
|
65
|
-
"@happy-dom/global-registrator": "
|
|
64
|
+
"@biomejs/biome": "2.5.15",
|
|
65
|
+
"@happy-dom/global-registrator": "21.0.1",
|
|
66
66
|
"@types/bun": "^1.4.2",
|
|
67
|
-
"happy-dom": "
|
|
68
|
-
"knip": "6.
|
|
67
|
+
"happy-dom": "21.0.1",
|
|
68
|
+
"knip": "6.41.0",
|
|
69
69
|
"playwright": "^1.63.0",
|
|
70
70
|
"typescript": "^7.0.2",
|
|
71
71
|
"vitepress": "^1.6.4"
|
package/src/cli/commands.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasErrors } from "../core/diagnostic.ts";
|
|
2
|
+
import { isRefName } from "../core/ref-name.ts";
|
|
2
3
|
import { mergeSarif, toSarif } from "../core/sarif.ts";
|
|
3
4
|
import type { CheckCliResult } from "./check.ts";
|
|
4
5
|
import type { ResultFields } from "./contract.ts";
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
formatMv,
|
|
22
23
|
formatNav,
|
|
23
24
|
formatNew,
|
|
25
|
+
formatPack,
|
|
24
26
|
formatPdf,
|
|
25
27
|
formatRef,
|
|
26
28
|
formatReport,
|
|
@@ -266,14 +268,22 @@ export const COMMANDS = {
|
|
|
266
268
|
new: result({
|
|
267
269
|
args: ["name"],
|
|
268
270
|
flags: ["theme-from"],
|
|
269
|
-
usage: ["dekc new <name> [--theme-from DECK]"],
|
|
270
|
-
summary:
|
|
271
|
+
usage: ["dekc new <name> [--theme-from DECK|REF]"],
|
|
272
|
+
summary:
|
|
273
|
+
"Add a deck with the project's theme, another deck's, or a ref's, and its skeleton slides.",
|
|
271
274
|
group: "Project",
|
|
272
|
-
overview: [["dekc new <name> [--theme-from DECK]", "add a deck"]],
|
|
273
|
-
agent: [
|
|
275
|
+
overview: [["dekc new <name> [--theme-from DECK|REF]", "add a deck"]],
|
|
276
|
+
agent: [
|
|
277
|
+
"dekc new <name> [--theme-from DECK|REF] a ref's theme to build on its layouts; copied, never linked",
|
|
278
|
+
],
|
|
274
279
|
run: async ({ cwd, args, flags }) => {
|
|
280
|
+
const themeFrom = flags["theme-from"];
|
|
275
281
|
const { newCommand } = await import("./new.ts");
|
|
276
|
-
|
|
282
|
+
if (themeFrom !== undefined && isRefName(themeFrom)) {
|
|
283
|
+
const { restoreRef } = await import("./ref.ts");
|
|
284
|
+
await restoreRef(cwd, themeFrom);
|
|
285
|
+
}
|
|
286
|
+
return newCommand({ cwd, name: args.name, themeFrom });
|
|
277
287
|
},
|
|
278
288
|
output: {
|
|
279
289
|
text: formatNew,
|
|
@@ -313,7 +323,7 @@ export const COMMANDS = {
|
|
|
313
323
|
ref: result({
|
|
314
324
|
args: ["source?"],
|
|
315
325
|
flags: [],
|
|
316
|
-
usage: ["dekc ref <owner/repo/deck>[@rev]", "dekc ref"],
|
|
326
|
+
usage: ["dekc ref <owner/repo/deck>[@rev]", "dekc ref <file>.tgz", "dekc ref"],
|
|
317
327
|
summary:
|
|
318
328
|
"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.",
|
|
319
329
|
group: "Refs",
|
|
@@ -322,11 +332,13 @@ export const COMMANDS = {
|
|
|
322
332
|
"dekc ref owner/repo/deck[@rev]",
|
|
323
333
|
"pin and fetch one (a GitHub link works too); again moves the pin",
|
|
324
334
|
],
|
|
335
|
+
["dekc ref <file>.tgz", "pin a pack someone sent as file/<deck>; commit packs/"],
|
|
325
336
|
["dekc ref", "list pinned refs"],
|
|
326
337
|
["dekc ref rm <ref>", "drop one"],
|
|
327
338
|
],
|
|
328
339
|
agent: [
|
|
329
340
|
"dekc ref [owner/repo/deck[@rev] | github-link] pin + fetch; no args lists; dekc ref rm <ref>",
|
|
341
|
+
"dekc ref <file>.tgz pin a pack as file/<deck>; commit packs/<deck>.tgz, which a clone unpacks",
|
|
330
342
|
],
|
|
331
343
|
run: async ({ cwd, args }) => {
|
|
332
344
|
const { addRef, listRefs } = await import("./ref.ts");
|
|
@@ -343,7 +355,31 @@ export const COMMANDS = {
|
|
|
343
355
|
},
|
|
344
356
|
},
|
|
345
357
|
},
|
|
346
|
-
output: {
|
|
358
|
+
output: {
|
|
359
|
+
text: formatRef,
|
|
360
|
+
paths: (data, display) =>
|
|
361
|
+
data.action === "add" && data.pack !== undefined
|
|
362
|
+
? { ...data, pack: display(data.pack) }
|
|
363
|
+
: data,
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
pack: result({
|
|
367
|
+
scope: "decks",
|
|
368
|
+
args: [],
|
|
369
|
+
flags: ["root-dist"],
|
|
370
|
+
usage: ["dekc pack [deck] [--root-dist]"],
|
|
371
|
+
summary:
|
|
372
|
+
"Write dist/<deck>.tgz: the deck as a file to hand to someone who reads it as a ref with\n`dekc ref <file>.tgz`, without access to where it lives.",
|
|
373
|
+
group: "Refs",
|
|
374
|
+
overview: [["dekc pack [--root-dist]", "write the deck as a file for `dekc ref` elsewhere"]],
|
|
375
|
+
agent: [
|
|
376
|
+
"dekc pack [--root-dist] dist/<deck>.tgz, for another project's `dekc ref <file>.tgz`",
|
|
377
|
+
],
|
|
378
|
+
run: async ({ target, flags }) => {
|
|
379
|
+
const { packCommand } = await import("./pack.ts");
|
|
380
|
+
return packCommand(target, { rootDist: flags["root-dist"] });
|
|
381
|
+
},
|
|
382
|
+
output: { text: formatPack },
|
|
347
383
|
}),
|
|
348
384
|
show: result({
|
|
349
385
|
scope: "deck",
|
|
@@ -774,7 +810,8 @@ export const COMMANDS = {
|
|
|
774
810
|
args: [],
|
|
775
811
|
flags: ["root-dist"],
|
|
776
812
|
usage: ["dekc pdf [deck] [--root-dist]"],
|
|
777
|
-
summary:
|
|
813
|
+
summary:
|
|
814
|
+
"Write dist/<deck>.pdf, one page per slide at its last beat: a picture of it with its text\nlaid over it, unseen, to select and search. Needs Playwright.",
|
|
778
815
|
group: "Output",
|
|
779
816
|
overview: [["dekc pdf [--root-dist]", "write a PDF"]],
|
|
780
817
|
agent: ["dekc pdf [--root-dist]"],
|
package/src/cli/contract.ts
CHANGED
|
@@ -111,6 +111,7 @@ export const RESULT_FIELDS = {
|
|
|
111
111
|
created: paths,
|
|
112
112
|
updated: paths,
|
|
113
113
|
next: z.array(z.string()),
|
|
114
|
+
warnings: z.array(z.string()).describe("Set when the theme came from a ref with no license."),
|
|
114
115
|
}),
|
|
115
116
|
ls: z.union([
|
|
116
117
|
z.strictObject({
|
|
@@ -159,6 +160,10 @@ export const RESULT_FIELDS = {
|
|
|
159
160
|
changed: z.boolean(),
|
|
160
161
|
dir: z.string(),
|
|
161
162
|
license: z.string().nullable(),
|
|
163
|
+
pack: z
|
|
164
|
+
.string()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("A pack's ref: the copy in packs/ to commit, which a clone unpacks it from."),
|
|
162
167
|
warnings: z.array(z.string()),
|
|
163
168
|
}),
|
|
164
169
|
z.strictObject({
|
|
@@ -413,6 +418,10 @@ export const RESULT_FIELDS = {
|
|
|
413
418
|
}),
|
|
414
419
|
pdf: z.strictObject({ outs: paths }),
|
|
415
420
|
pptx: z.strictObject({ outs: paths }),
|
|
421
|
+
pack: z.strictObject({
|
|
422
|
+
outs: paths,
|
|
423
|
+
warnings: z.array(z.string()).describe("Once per project: the packs carry no license."),
|
|
424
|
+
}),
|
|
416
425
|
} satisfies Record<ResultCommand, FieldsSchema>;
|
|
417
426
|
|
|
418
427
|
type FieldsSchema = z.ZodObject | z.ZodUnion<readonly z.ZodObject[]>;
|
package/src/cli/files.ts
CHANGED
|
@@ -14,7 +14,14 @@ export function defaultTheme(): string {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function defaultToml(): string {
|
|
17
|
-
return
|
|
17
|
+
return `# dek project
|
|
18
|
+
|
|
19
|
+
[pack]
|
|
20
|
+
# The license \`dekc pack\` hands a deck over under. CC0-1.0 lets the reader reuse it
|
|
21
|
+
# freely, with no credit owed, as a template is meant to be used; CC-BY-4.0 asks for
|
|
22
|
+
# credit. Delete the line to hand decks over with no license.
|
|
23
|
+
license = "CC0-1.0"
|
|
24
|
+
`;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
/**
|
package/src/cli/new.ts
CHANGED
|
@@ -4,9 +4,10 @@ import { loadConfig } from "../core/config.ts";
|
|
|
4
4
|
import { deckPaths } from "../core/deck-paths.ts";
|
|
5
5
|
import { DekError } from "../core/error.ts";
|
|
6
6
|
import { DECK_NAME_HINT, isDeckName } from "../core/path.ts";
|
|
7
|
+
import { isRefName } from "../core/ref-name.ts";
|
|
7
8
|
import { syncDeck } from "../core/sync.ts";
|
|
8
9
|
import { applyPlan, deckPlan, nextSteps } from "./files.ts";
|
|
9
|
-
import { requireDeckFromCwd, requireProject } from "./scope.ts";
|
|
10
|
+
import { type RefInfo, requireDeckFromCwd, requireProject, resolveTarget } from "./scope.ts";
|
|
10
11
|
|
|
11
12
|
export type NewResult = {
|
|
12
13
|
name: string;
|
|
@@ -16,6 +17,8 @@ export type NewResult = {
|
|
|
16
17
|
updated: string[];
|
|
17
18
|
/** The commands to run next, from the directory new ran in. */
|
|
18
19
|
next: string[];
|
|
20
|
+
/** Set when the theme came from a ref with no license. */
|
|
21
|
+
warnings: string[];
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
export function newCommand(options: { cwd: string; name: string; themeFrom?: string }): NewResult {
|
|
@@ -35,19 +38,29 @@ export function newCommand(options: { cwd: string; name: string; themeFrom?: str
|
|
|
35
38
|
});
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
// A ref is read where it is, never written: only the copy below is the new deck's.
|
|
42
|
+
const fromRef =
|
|
43
|
+
options.themeFrom !== undefined && isRefName(options.themeFrom)
|
|
44
|
+
? resolveTarget(options.cwd, "deck", { refs: true, deck: options.themeFrom })
|
|
45
|
+
: undefined;
|
|
46
|
+
const ref = fromRef?.ref;
|
|
47
|
+
const themeSource = fromRef
|
|
48
|
+
? deckPaths(fromRef.deck.dir).theme
|
|
49
|
+
: options.themeFrom
|
|
50
|
+
? deckPaths(requireDeckFromCwd(options.cwd, options.themeFrom).deck.dir).theme
|
|
51
|
+
: join(project.root, "theme.css");
|
|
41
52
|
if (!existsSync(themeSource)) {
|
|
42
53
|
throw new DekError("theme.css not found", {
|
|
43
54
|
path: themeSource,
|
|
44
|
-
hint:
|
|
45
|
-
? "run `dekc
|
|
46
|
-
:
|
|
55
|
+
hint: ref
|
|
56
|
+
? "run `dekc ref` to list the refs, and pick one whose deck has theme.css"
|
|
57
|
+
: options.themeFrom
|
|
58
|
+
? "run `dekc ls` and pick a deck that has theme.css"
|
|
59
|
+
: "add theme.css at the project root",
|
|
47
60
|
});
|
|
48
61
|
}
|
|
49
62
|
|
|
50
|
-
const theme = readFileSync(themeSource, "utf8")
|
|
63
|
+
const theme = `${ref ? provenance(ref) : ""}${readFileSync(themeSource, "utf8")}`;
|
|
51
64
|
const { created } = applyPlan(
|
|
52
65
|
deckPlan(project.root, name, theme, loadConfig(project.configPath)),
|
|
53
66
|
);
|
|
@@ -61,5 +74,16 @@ export function newCommand(options: { cwd: string; name: string; themeFrom?: str
|
|
|
61
74
|
created,
|
|
62
75
|
updated: synced.dekFiles.updated,
|
|
63
76
|
next: nextSteps(options.cwd, project.root, dir),
|
|
77
|
+
warnings:
|
|
78
|
+
ref && ref.license === null
|
|
79
|
+
? [
|
|
80
|
+
`${ref.name} has no license; ask its author before you present or share a deck built on its theme`,
|
|
81
|
+
]
|
|
82
|
+
: [],
|
|
64
83
|
};
|
|
65
84
|
}
|
|
85
|
+
|
|
86
|
+
/** The first line of a theme copied from a ref: where it came from, for its reader and its credit. */
|
|
87
|
+
function provenance(ref: RefInfo): string {
|
|
88
|
+
return `/* Theme from the ref ${ref.name} at ${ref.rev.slice(0, 7)}. */\n`;
|
|
89
|
+
}
|
package/src/cli/pack.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { writePack } from "../core/pack.ts";
|
|
2
|
+
import type { DecksTarget } from "./scope.ts";
|
|
3
|
+
|
|
4
|
+
/** One file per deck, a single deck included, as `dekc pdf` answers, and what to fix first. */
|
|
5
|
+
export type PackCliResult = { outs: string[]; warnings: string[] };
|
|
6
|
+
|
|
7
|
+
export async function packCommand(
|
|
8
|
+
{ project, decks }: DecksTarget,
|
|
9
|
+
options: { rootDist?: boolean } = {},
|
|
10
|
+
): Promise<PackCliResult> {
|
|
11
|
+
const outs: string[] = [];
|
|
12
|
+
const warnings = new Set<string>();
|
|
13
|
+
for (const entry of decks) {
|
|
14
|
+
const done = await writePack(project, entry, { rootDist: options.rootDist });
|
|
15
|
+
outs.push(done.outPath);
|
|
16
|
+
for (const warning of done.warnings) {
|
|
17
|
+
warnings.add(warning);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { outs, warnings: [...warnings] };
|
|
21
|
+
}
|
package/src/cli/ref.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readdirSync, readFileSync, rmdirSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { loadConfig } from "../core/config.ts";
|
|
4
4
|
import { DekError } from "../core/error.ts";
|
|
5
5
|
import { downloadTarball, resolveRev } from "../core/github.ts";
|
|
6
6
|
import {
|
|
7
|
+
deckOfPack,
|
|
7
8
|
installSnapshot,
|
|
8
9
|
isPinnedRev,
|
|
10
|
+
packFile,
|
|
9
11
|
parseRefSource,
|
|
10
|
-
|
|
12
|
+
pickSnapshot,
|
|
13
|
+
readPack,
|
|
11
14
|
readTarball,
|
|
12
15
|
refLicense,
|
|
13
16
|
refState,
|
|
14
17
|
refTitle,
|
|
18
|
+
type Snapshot,
|
|
19
|
+
snapshotDigest,
|
|
20
|
+
writeSnapshot,
|
|
15
21
|
} from "../core/ref.ts";
|
|
22
|
+
import type { FileRefSource, GithubRefSource } from "../core/ref-name.ts";
|
|
16
23
|
import { readTextIfExists } from "../core/resolve.ts";
|
|
17
|
-
import { readSourceIfExists, removeInside, writeInside } from "../core/safe-fs.ts";
|
|
24
|
+
import { readSourceIfExists, removeInside, requireInside, writeInside } from "../core/safe-fs.ts";
|
|
18
25
|
import { writeAgentsMd } from "../core/sync.ts";
|
|
19
26
|
import { setTableKey } from "../core/toml-keys.ts";
|
|
20
27
|
import { requireProject } from "./scope.ts";
|
|
@@ -29,6 +36,8 @@ export type RefAddResult = {
|
|
|
29
36
|
changed: boolean;
|
|
30
37
|
dir: string;
|
|
31
38
|
license: string | null;
|
|
39
|
+
/** A pack's ref: its committed copy, which a clone unpacks the ref from. */
|
|
40
|
+
pack?: string;
|
|
32
41
|
warnings: string[];
|
|
33
42
|
};
|
|
34
43
|
|
|
@@ -53,17 +62,71 @@ export async function restoreRef(cwd: string, arg: string): Promise<void> {
|
|
|
53
62
|
if (pinned === undefined || fetched) {
|
|
54
63
|
return;
|
|
55
64
|
}
|
|
65
|
+
if (source.kind === "file") {
|
|
66
|
+
await unpackSnapshot(project.root, source, pinned);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
56
69
|
await fetchSnapshot(project.root, source, pinned);
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
async function fetchSnapshot(root: string, source:
|
|
72
|
+
async function fetchSnapshot(root: string, source: GithubRefSource, sha: string) {
|
|
60
73
|
const files = await readTarball(await downloadTarball(source.owner, source.repo, sha));
|
|
61
74
|
return installSnapshot(root, source, sha, files);
|
|
62
75
|
}
|
|
63
76
|
|
|
77
|
+
/** A pack's snapshot, unpacked again from its committed copy, which must be the pinned one. */
|
|
78
|
+
async function unpackSnapshot(root: string, source: FileRefSource, pin: string): Promise<void> {
|
|
79
|
+
const rel = packFile(source.deck);
|
|
80
|
+
const path = join(root, rel);
|
|
81
|
+
if (!existsSync(path)) {
|
|
82
|
+
throw new DekError(`${rel} is missing`, {
|
|
83
|
+
path,
|
|
84
|
+
hint: `a pack's ref is unpacked from its copy in packs/, so commit packs/ with dek.toml; run \`dekc ref <file>.tgz\` with the pack you were sent to add it again`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const snapshot = await readPackFile(path, source, root);
|
|
88
|
+
if ((await snapshotDigest(snapshot)) !== pin) {
|
|
89
|
+
throw new DekError(`${rel} is not the pack ${source.name} is pinned to`, {
|
|
90
|
+
path,
|
|
91
|
+
hint: `run \`dekc ref ${rel}\` to pin the pack that is there, or put back the one dek.toml pins`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
await writeSnapshot(root, source.name, pin, snapshot);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** What a snapshot keeps of the pack at `path`; an error says which file it was about. */
|
|
98
|
+
async function readPackFile(
|
|
99
|
+
path: string,
|
|
100
|
+
source: FileRefSource | undefined,
|
|
101
|
+
root?: string,
|
|
102
|
+
): Promise<Snapshot & { source: FileRefSource; bytes: Uint8Array }> {
|
|
103
|
+
try {
|
|
104
|
+
const bytes = readFileSync(root === undefined ? path : requireInside(path, root));
|
|
105
|
+
const files = await readPack(bytes);
|
|
106
|
+
const found = source ?? parseRefSource(`file/${deckOfPack(files)}`);
|
|
107
|
+
if (found.kind !== "file") {
|
|
108
|
+
throw new DekError("the pack's deck cannot be named as a ref");
|
|
109
|
+
}
|
|
110
|
+
return { ...pickSnapshot(files, found), source: found, bytes };
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof DekError && error.path === undefined) {
|
|
113
|
+
throw new DekError(error.message, { path, ...(error.hint ? { hint: error.hint } : {}) });
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
64
119
|
/** `dekc ref <source>`: pin and fetch, or move the pin to the latest or to the rev after `@`. */
|
|
65
120
|
export async function addRef(cwd: string, arg: string): Promise<RefAddResult> {
|
|
121
|
+
if (isPackPath(arg)) {
|
|
122
|
+
return addPack(cwd, arg);
|
|
123
|
+
}
|
|
66
124
|
const source = parseRefSource(arg);
|
|
125
|
+
if (source.kind === "file") {
|
|
126
|
+
throw new DekError(`"${arg}" names a pack's ref`, {
|
|
127
|
+
hint: "add a pack by its file, `dekc ref <file>.tgz`; `dekc ref` lists the refs already added",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
67
130
|
const project = requireProject(cwd);
|
|
68
131
|
const { pinned: from, dir, fetched } = refState(project, source.name);
|
|
69
132
|
const rev =
|
|
@@ -103,6 +166,84 @@ export async function addRef(cwd: string, arg: string): Promise<RefAddResult> {
|
|
|
103
166
|
};
|
|
104
167
|
}
|
|
105
168
|
|
|
169
|
+
/** An argument that ends in .tgz or .tar.gz is a pack's path, never a ref name. */
|
|
170
|
+
function isPackPath(arg: string): boolean {
|
|
171
|
+
return /\.(tgz|tar\.gz)$/i.test(arg);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* `dekc ref <file>.tgz`: pin the pack's deck as `file/<deck>` to the digest of its files, keep
|
|
176
|
+
* the pack as packs/<deck>.tgz for git, and unpack the snapshot. Everything is read and checked
|
|
177
|
+
* before the first write.
|
|
178
|
+
*/
|
|
179
|
+
async function addPack(cwd: string, arg: string): Promise<RefAddResult> {
|
|
180
|
+
const project = requireProject(cwd);
|
|
181
|
+
const given = resolve(cwd, arg);
|
|
182
|
+
if (!existsSync(given)) {
|
|
183
|
+
throw new DekError(`no such file: ${arg}`, {
|
|
184
|
+
path: given,
|
|
185
|
+
hint: "pass the path of the .tgz `dekc pack` wrote",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const read = await readPackFile(given, undefined);
|
|
189
|
+
const { source, bytes } = read;
|
|
190
|
+
const rev = await snapshotDigest(read);
|
|
191
|
+
const { pinned: from, dir, fetched } = refState(project, source.name);
|
|
192
|
+
const stored = join(project.root, packFile(source.deck));
|
|
193
|
+
|
|
194
|
+
let changed = false;
|
|
195
|
+
if (!(fetched && from === rev)) {
|
|
196
|
+
await writeSnapshot(project.root, source.name, rev, read);
|
|
197
|
+
changed = true;
|
|
198
|
+
}
|
|
199
|
+
if (!(await isPackOf(stored, source, rev, project.root))) {
|
|
200
|
+
writeInside(stored, bytes, project.root);
|
|
201
|
+
changed = true;
|
|
202
|
+
}
|
|
203
|
+
if (from !== rev) {
|
|
204
|
+
const toml = readTextIfExists(project.configPath) ?? "";
|
|
205
|
+
writeInside(project.configPath, setTableKey(toml, "refs", source.name, rev), project.root);
|
|
206
|
+
changed = true;
|
|
207
|
+
}
|
|
208
|
+
ignoreRefs(project.root);
|
|
209
|
+
writeAgentsMd(project.root);
|
|
210
|
+
|
|
211
|
+
const license = refLicense(dir);
|
|
212
|
+
return {
|
|
213
|
+
action: "add",
|
|
214
|
+
name: source.name,
|
|
215
|
+
rev,
|
|
216
|
+
...(from !== undefined && from !== rev ? { from } : {}),
|
|
217
|
+
changed,
|
|
218
|
+
dir,
|
|
219
|
+
license,
|
|
220
|
+
pack: stored,
|
|
221
|
+
warnings:
|
|
222
|
+
license === null
|
|
223
|
+
? [
|
|
224
|
+
"the pack has no license; read it as a model, but ask its author before reusing its text or assets",
|
|
225
|
+
]
|
|
226
|
+
: [],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** True when the pack at `path` holds the files `rev` pins. */
|
|
231
|
+
async function isPackOf(
|
|
232
|
+
path: string,
|
|
233
|
+
source: FileRefSource,
|
|
234
|
+
rev: string,
|
|
235
|
+
root: string,
|
|
236
|
+
): Promise<boolean> {
|
|
237
|
+
if (!existsSync(path)) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
return (await snapshotDigest(await readPackFile(path, source, root))) === rev;
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
106
247
|
export function listRefs(cwd: string): RefListResult {
|
|
107
248
|
const project = requireProject(cwd);
|
|
108
249
|
const refs = Object.keys(loadConfig(project.configPath).refs ?? {}).map((name) => {
|
|
@@ -133,6 +274,17 @@ export function removeRef(cwd: string, arg: string): RefRmResult {
|
|
|
133
274
|
}
|
|
134
275
|
removeInside(dir, project.root, { recursive: true });
|
|
135
276
|
removeEmptyParents(dirname(dir), project.root);
|
|
277
|
+
if (source.kind === "file") {
|
|
278
|
+
const packs = join(project.root, "packs");
|
|
279
|
+
removeInside(join(project.root, packFile(source.deck)), project.root);
|
|
280
|
+
if (
|
|
281
|
+
existsSync(packs) &&
|
|
282
|
+
!lstatSync(packs).isSymbolicLink() &&
|
|
283
|
+
readdirSync(packs).length === 0
|
|
284
|
+
) {
|
|
285
|
+
rmdirSync(packs);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
136
288
|
writeAgentsMd(project.root);
|
|
137
289
|
return { action: "rm", name: source.name };
|
|
138
290
|
}
|
package/src/cli/scope.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { DekError } from "../core/error.ts";
|
|
4
4
|
import {
|
|
5
5
|
isRefName,
|
|
6
|
+
packFile,
|
|
6
7
|
parseRefSource,
|
|
7
8
|
REF_MARKER,
|
|
8
9
|
readRefMeta,
|
|
@@ -139,7 +140,7 @@ export function resolveScope(cwd: string, options: { deck?: string } = {}): Scop
|
|
|
139
140
|
}
|
|
140
141
|
if (isRefName(name)) {
|
|
141
142
|
throw new DekError(`"${name}" is a ref; refs are read-only`, {
|
|
142
|
-
hint: `read it with \`dekc show ${name} <slug
|
|
143
|
+
hint: `start a deck on its theme with \`dekc new <name> --theme-from ${name}\`, or read it with \`dekc show ${name} <slug>\` and write what you need in your own deck's theme`,
|
|
143
144
|
});
|
|
144
145
|
}
|
|
145
146
|
const deck = project.decks.find((entry) => entry.name === name);
|
|
@@ -273,10 +274,18 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
273
274
|
if (pinned === undefined) {
|
|
274
275
|
throw new DekError(`ref "${source.name}" is not added`, {
|
|
275
276
|
path: project.configPath,
|
|
276
|
-
hint:
|
|
277
|
+
hint:
|
|
278
|
+
source.kind === "file"
|
|
279
|
+
? "add it from the pack you were sent: `dekc ref <file>.tgz`"
|
|
280
|
+
: `run \`dekc ref ${arg}\``,
|
|
277
281
|
});
|
|
278
282
|
}
|
|
279
|
-
|
|
283
|
+
// What adds the ref again: a pack's ref from its file, any other from its name.
|
|
284
|
+
const again =
|
|
285
|
+
source.kind === "file"
|
|
286
|
+
? `dekc ref ${packFile(source.deck)}`
|
|
287
|
+
: `dekc ref ${source.name}@${pinned}`;
|
|
288
|
+
if (source.kind === "github" && source.rev !== undefined && source.rev !== pinned) {
|
|
280
289
|
throw new DekError(`"${arg}" is not the pinned version, ${pinned.slice(0, 7)}`, {
|
|
281
290
|
path: project.configPath,
|
|
282
291
|
hint: `run \`dekc ref ${arg}\` to pin that version, or drop @${source.rev}`,
|
|
@@ -285,7 +294,7 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
285
294
|
if (!fetched) {
|
|
286
295
|
throw new DekError(`ref "${source.name}" is not fetched`, {
|
|
287
296
|
path: dir,
|
|
288
|
-
hint: `run
|
|
297
|
+
hint: `run \`${again}\``,
|
|
289
298
|
});
|
|
290
299
|
}
|
|
291
300
|
const snapshot = resolveProject(dir);
|
|
@@ -297,7 +306,7 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
297
306
|
}
|
|
298
307
|
throw new DekError(`ref "${source.name}" has no deck "${source.deck}"`, {
|
|
299
308
|
path: dir,
|
|
300
|
-
hint: `run
|
|
309
|
+
hint: `run \`${again}\` to fetch it again`,
|
|
301
310
|
});
|
|
302
311
|
}
|
|
303
312
|
return {
|
package/src/cli/text.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// How each command's result reads as text; commands.ts says which command prints which.
|
|
2
2
|
// The pieces the dev server's log shares, such as diagnostics and tables, are in format.ts.
|
|
3
3
|
import type { Diagnostic, SkippedCheck } from "../core/diagnostic.ts";
|
|
4
|
+
import { packFile, parseRefSource } from "../core/ref-name.ts";
|
|
4
5
|
import { formatClock } from "../core/timing.ts";
|
|
5
6
|
import type { AnnotationsCliResult } from "./annotations.ts";
|
|
6
7
|
import type { BuildCliResult } from "./build.ts";
|
|
@@ -13,6 +14,7 @@ import type { LsDeckResult, LsListResult } from "./ls.ts";
|
|
|
13
14
|
import type { MarksCliResult } from "./marks.ts";
|
|
14
15
|
import type { MvResult } from "./mv.ts";
|
|
15
16
|
import type { NewResult } from "./new.ts";
|
|
17
|
+
import type { PackCliResult } from "./pack.ts";
|
|
16
18
|
import type { PdfCliResult } from "./pdf.ts";
|
|
17
19
|
import type { RefCliResult } from "./ref.ts";
|
|
18
20
|
import type { ShotCliResult } from "./shot.ts";
|
|
@@ -130,7 +132,8 @@ export function formatInit(data: InitResult): string {
|
|
|
130
132
|
/** The deck, and any of dek's own files it brought up to date: the rest is the deck's, and new. */
|
|
131
133
|
export function formatNew(data: NewResult): string {
|
|
132
134
|
const updated = data.updated.map((path) => ` ${path} (updated)`);
|
|
133
|
-
|
|
135
|
+
const warnings = data.warnings.map((warning) => `warning: ${warning}`);
|
|
136
|
+
return withNext([`created deck ${data.name}`, ...updated, ...warnings].join("\n"), data.next);
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
export function formatMv(data: MvResult): string {
|
|
@@ -227,6 +230,10 @@ export function formatPdf(data: PdfCliResult): string {
|
|
|
227
230
|
return data.outs.map((path) => `wrote ${path}`).join("\n");
|
|
228
231
|
}
|
|
229
232
|
|
|
233
|
+
export function formatPack(data: PackCliResult): string {
|
|
234
|
+
return [formatPdf(data), ...data.warnings.map((warning) => `warning: ${warning}`)].join("\n");
|
|
235
|
+
}
|
|
236
|
+
|
|
230
237
|
export function formatLs(data: LsListResult | LsDeckResult): string {
|
|
231
238
|
switch (data.kind) {
|
|
232
239
|
case "list": {
|
|
@@ -409,6 +416,14 @@ export function formatShow(data: ShowResult): string {
|
|
|
409
416
|
.join("\n\n");
|
|
410
417
|
}
|
|
411
418
|
|
|
419
|
+
/** What a read does for a ref whose snapshot is not there: fetch it, or unpack its pack. */
|
|
420
|
+
function notFetched(name: string): string {
|
|
421
|
+
const source = parseRefSource(name);
|
|
422
|
+
return source.kind === "file"
|
|
423
|
+
? `(not unpacked; a read unpacks ${packFile(source.deck)})`
|
|
424
|
+
: "(not fetched; a read fetches it)";
|
|
425
|
+
}
|
|
426
|
+
|
|
412
427
|
export function formatRef(data: RefCliResult): string {
|
|
413
428
|
const short = (rev: string) => rev.slice(0, 7);
|
|
414
429
|
switch (data.action) {
|
|
@@ -421,6 +436,7 @@ export function formatRef(data: RefCliResult): string {
|
|
|
421
436
|
return [
|
|
422
437
|
head,
|
|
423
438
|
...data.warnings.map((warning) => `warning: ${warning}`),
|
|
439
|
+
...(data.pack ? [`commit ${data.pack}: a clone unpacks the ref from it`] : []),
|
|
424
440
|
`read it with \`dekc ls ${data.name}\` and \`dekc show ${data.name} <slug>\``,
|
|
425
441
|
].join("\n");
|
|
426
442
|
}
|
|
@@ -433,7 +449,7 @@ export function formatRef(data: RefCliResult): string {
|
|
|
433
449
|
data.refs.map((ref) => [
|
|
434
450
|
ref.name,
|
|
435
451
|
short(ref.rev),
|
|
436
|
-
ref.fetched ? (ref.title ?? "") :
|
|
452
|
+
ref.fetched ? (ref.title ?? "") : notFetched(ref.name),
|
|
437
453
|
ref.slides === undefined ? "" : String(ref.slides),
|
|
438
454
|
]),
|
|
439
455
|
["left", "left", "left", "right"],
|