@liebstoeckel/cli 0.3.7
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 +373 -0
- package/README.md +74 -0
- package/package.json +60 -0
- package/skill/AGENTS.md +18 -0
- package/skill/SKILL.md +144 -0
- package/skill/references/authoring.md +81 -0
- package/skill/references/build-plugins.md +116 -0
- package/skill/references/components.md +107 -0
- package/skill/references/editing.md +78 -0
- package/skill/references/plugins.md +121 -0
- package/skill/references/troubleshooting.md +39 -0
- package/src/add.ts +369 -0
- package/src/build.ts +354 -0
- package/src/cli.ts +78 -0
- package/src/cloud.ts +570 -0
- package/src/creds.ts +31 -0
- package/src/new.ts +310 -0
- package/src/registry.ts +124 -0
- package/src/skill.ts +167 -0
- package/src/targeting.ts +8 -0
- package/src/trust.ts +73 -0
- package/src/update.ts +148 -0
package/src/build.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// `liebstoeckel build|eject|pack|licenses`, the deck build/inspect commands.
|
|
2
|
+
// Heavy engine/thumbnails modules are imported lazily inside each `run` so the
|
|
3
|
+
// umbrella pays for them only when the command is actually invoked.
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { resolve, basename, join } from "node:path";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { readdir } from "node:fs/promises";
|
|
8
|
+
import type { Dirent } from "node:fs";
|
|
9
|
+
import { looksLikeDeck } from "./targeting";
|
|
10
|
+
import { ensureBuildTrust } from "./trust";
|
|
11
|
+
|
|
12
|
+
/** Deck targeting (ADR 0050): a leading positional, else `--dir`, else cwd. */
|
|
13
|
+
const deckDir = (args: { deck?: string; dir?: string }): string => args.deck ?? args.dir ?? ".";
|
|
14
|
+
|
|
15
|
+
/** Gate a build behind first-time trust: building a deck runs its build-time code on this
|
|
16
|
+
* machine (Bun macros, build plugins) with full FS/network access. Decks scaffolded here
|
|
17
|
+
* pass silently; an unfamiliar one is confirmed once (or pre-approved via `--trust` /
|
|
18
|
+
* `LIEBSTOECKEL_TRUST_BUILD=1`). Non-interactive without approval refuses, fail-closed.
|
|
19
|
+
* Exits the process on a no. */
|
|
20
|
+
async function gateBuildTrust(dir: string, trustFlag: boolean | undefined, json = false): Promise<void> {
|
|
21
|
+
const abs = resolve(dir);
|
|
22
|
+
const preapproved = trustFlag === true || process.env.LIEBSTOECKEL_TRUST_BUILD === "1";
|
|
23
|
+
const interactive = !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
24
|
+
const ok = await ensureBuildTrust(abs, {
|
|
25
|
+
preapproved,
|
|
26
|
+
confirm: interactive
|
|
27
|
+
? (d) => {
|
|
28
|
+
console.error(
|
|
29
|
+
`\n⚠ Building a deck runs its code on your machine.\n` +
|
|
30
|
+
` A liebstoeckel deck is real code: building it executes the deck's build-time\n` +
|
|
31
|
+
` modules (Bun macros, build plugins) with full access to your files and network.\n` +
|
|
32
|
+
` Only build decks you trust.\n\n Deck: ${d}`,
|
|
33
|
+
);
|
|
34
|
+
// Bun's confirm() reads a yes/no from the TTY; defaults to no on empty/EOF.
|
|
35
|
+
return confirm(" Trust this deck and build it?");
|
|
36
|
+
}
|
|
37
|
+
: undefined,
|
|
38
|
+
});
|
|
39
|
+
if (ok) return;
|
|
40
|
+
if (interactive) {
|
|
41
|
+
console.error("✕ build aborted: deck not trusted.");
|
|
42
|
+
} else {
|
|
43
|
+
// Carry the failure as JSON on stdout when a machine asked for it (ADR 0045) so an
|
|
44
|
+
// agent gets a structured signal instead of empty output — but frame trust as a HUMAN
|
|
45
|
+
// decision, NOT a flag to self-apply. Approving an untrusted deck is the user's call;
|
|
46
|
+
// an agent that pastes --trust on its own has bypassed the gate, so don't advertise it
|
|
47
|
+
// as the remedy here.
|
|
48
|
+
if (json) {
|
|
49
|
+
console.log(
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
ok: false,
|
|
52
|
+
error: "untrusted deck",
|
|
53
|
+
hint: `building runs this deck's code on the machine — a human who trusts ${abs} must approve it; an agent must not self-approve`,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
console.error(
|
|
58
|
+
`✕ refusing to build an untrusted deck non-interactively.\n` +
|
|
59
|
+
` Building runs this deck's code on this machine — trusting it is a decision for a\n` +
|
|
60
|
+
` human who has reviewed ${abs}, not for an agent to make on its own.\n` +
|
|
61
|
+
` A human can approve it with --trust (or LIEBSTOECKEL_TRUST_BUILD=1).`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Best-effort scan of a deck's source for speaker notes. Notes are compiled into the
|
|
68
|
+
* built .html and are NOT hidden from a live audience, so the author deserves a heads-up.
|
|
69
|
+
* Prunes node_modules/dist/dot-dirs and stops at the first hit. */
|
|
70
|
+
async function deckHasSpeakerNotes(dir: string): Promise<boolean> {
|
|
71
|
+
const NOTES = /export\s+(?:const|let|var|function)\s+notes\b|^\s*notes\s*:/m;
|
|
72
|
+
const SKIP = new Set(["node_modules", "dist", ".git"]);
|
|
73
|
+
const stack = [dir];
|
|
74
|
+
while (stack.length) {
|
|
75
|
+
const d = stack.pop()!;
|
|
76
|
+
let entries: Dirent[];
|
|
77
|
+
try {
|
|
78
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const e of entries) {
|
|
83
|
+
if (e.isDirectory()) {
|
|
84
|
+
if (!SKIP.has(e.name) && !e.name.startsWith(".")) stack.push(join(d, e.name));
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (/\.(mdx?|tsx?|jsx?)$/.test(e.name)) {
|
|
88
|
+
try {
|
|
89
|
+
if (NOTES.test(await Bun.file(join(d, e.name)).text())) return true;
|
|
90
|
+
} catch {
|
|
91
|
+
/* ignore unreadable file */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function warnIfSpeakerNotes(dir: string): Promise<void> {
|
|
100
|
+
if (await deckHasSpeakerNotes(dir)) {
|
|
101
|
+
console.error(
|
|
102
|
+
`⚠ This deck includes speaker notes. Speaker notes are bundled into the built\n` +
|
|
103
|
+
` .html and are NOT hidden from a live audience — anyone with the viewer link\n` +
|
|
104
|
+
` can read them. Don't put confidential content in speaker notes.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const buildCommand = defineCommand({
|
|
110
|
+
meta: {
|
|
111
|
+
name: "build",
|
|
112
|
+
description: "build a deck → one self-contained .html (+ thumbnails)",
|
|
113
|
+
},
|
|
114
|
+
args: {
|
|
115
|
+
deck: { type: "positional", required: false, description: "deck directory (default: cwd)", valueHint: "dir" },
|
|
116
|
+
dir: { type: "string", description: "deck directory (alternative to the positional)", valueHint: "deck" },
|
|
117
|
+
"inline-package": {
|
|
118
|
+
type: "boolean",
|
|
119
|
+
default: true,
|
|
120
|
+
description: "embed the recoverable source package",
|
|
121
|
+
negativeDescription: "do not embed the source package",
|
|
122
|
+
},
|
|
123
|
+
"inline-licenses": {
|
|
124
|
+
type: "boolean",
|
|
125
|
+
default: true,
|
|
126
|
+
description: "embed third-party license notices",
|
|
127
|
+
negativeDescription: "do not embed license notices",
|
|
128
|
+
},
|
|
129
|
+
"allow-secret": { type: "boolean", description: "allow packing files outside the deck's `files` allowlist" },
|
|
130
|
+
check: { type: "boolean", description: "validate the deck bundles without writing an artifact" },
|
|
131
|
+
trust: {
|
|
132
|
+
type: "boolean",
|
|
133
|
+
description: "trust this deck's build-time code (a deck is code; remembered after the first build)",
|
|
134
|
+
},
|
|
135
|
+
json: { type: "boolean", description: "machine-readable JSON output (default when piped)" },
|
|
136
|
+
},
|
|
137
|
+
async run({ args }) {
|
|
138
|
+
const dir = deckDir(args);
|
|
139
|
+
// JSON output is requested explicitly or whenever stdout isn't a TTY (agent contract).
|
|
140
|
+
const json = !!args.json || !process.stdout.isTTY;
|
|
141
|
+
// Building runs the deck's build-time code on this machine — confirm trust first.
|
|
142
|
+
await gateBuildTrust(dir, args.trust, json);
|
|
143
|
+
const prev = process.cwd();
|
|
144
|
+
process.chdir(resolve(dir)); // resolve(".") = cwd, so the default is a no-op
|
|
145
|
+
try {
|
|
146
|
+
// `--check`: validate the deck bundles (no artifact, no thumbnails) and report
|
|
147
|
+
// structured diagnostics for an agent's fix loop (ADR 0045).
|
|
148
|
+
if (args.check) {
|
|
149
|
+
const { checkDeck } = await import("@liebstoeckel/engine/build");
|
|
150
|
+
const { ok, diagnostics } = await checkDeck({ entry: "./index.html" });
|
|
151
|
+
if (json) {
|
|
152
|
+
console.log(JSON.stringify({ ok, diagnostics }, null, 2));
|
|
153
|
+
} else if (ok) {
|
|
154
|
+
console.log("✓ deck builds (check passed)");
|
|
155
|
+
} else {
|
|
156
|
+
for (const d of diagnostics) {
|
|
157
|
+
const loc = d.file ? ` ${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
158
|
+
console.error(`✕${loc} ${d.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!ok) process.exit(1);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await warnIfSpeakerNotes(".");
|
|
166
|
+
const { buildDeck } = await import("@liebstoeckel/thumbnails/build");
|
|
167
|
+
const { cliVersion } = await import("./skill");
|
|
168
|
+
// In JSON mode stdout must be a single machine-readable object, so route the
|
|
169
|
+
// build's human progress prose (✓ built…, license/source/thumbnail notes) to
|
|
170
|
+
// stderr for the duration and print the structured result to stdout at the end.
|
|
171
|
+
const realLog = console.log;
|
|
172
|
+
if (json) console.log = (...a: unknown[]) => console.error(...a);
|
|
173
|
+
let result;
|
|
174
|
+
try {
|
|
175
|
+
result = await buildDeck({
|
|
176
|
+
entry: "./index.html",
|
|
177
|
+
outdir: "./dist",
|
|
178
|
+
inlinePackage: args.inlinePackage !== false,
|
|
179
|
+
inlineLicenses: args.inlineLicenses !== false,
|
|
180
|
+
allowSecret: !!args.allowSecret,
|
|
181
|
+
generator: { name: "cli", version: await cliVersion() },
|
|
182
|
+
});
|
|
183
|
+
} finally {
|
|
184
|
+
console.log = realLog;
|
|
185
|
+
}
|
|
186
|
+
if (json) {
|
|
187
|
+
console.log(
|
|
188
|
+
JSON.stringify({
|
|
189
|
+
ok: true,
|
|
190
|
+
artifact: resolve(result.artifact),
|
|
191
|
+
outfile: result.outfile,
|
|
192
|
+
thumbnails: result.thumbnails,
|
|
193
|
+
...(result.thumbnailsSkipped ? { thumbnailsSkipped: result.thumbnailsSkipped } : {}),
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
process.chdir(prev);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
export const ejectCommand = defineCommand({
|
|
204
|
+
meta: {
|
|
205
|
+
name: "eject",
|
|
206
|
+
description: "recover a built deck's editable source",
|
|
207
|
+
},
|
|
208
|
+
args: {
|
|
209
|
+
deck: { type: "positional", required: false, description: "built deck .html", valueHint: "deck.html" },
|
|
210
|
+
outdir: { type: "positional", required: false, description: "output directory", valueHint: "outdir" },
|
|
211
|
+
force: { type: "boolean", description: "overwrite an existing output directory" },
|
|
212
|
+
},
|
|
213
|
+
async run({ args }) {
|
|
214
|
+
const htmlPath = args.deck;
|
|
215
|
+
if (!htmlPath) {
|
|
216
|
+
console.error("usage: liebstoeckel eject <deck.html> [outdir] [--force]");
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
const outDir = args.outdir ?? resolve(basename(htmlPath).replace(/\.html?$/i, "") + "-source");
|
|
220
|
+
const { ejectSource } = await import("@liebstoeckel/engine/build/source-package");
|
|
221
|
+
try {
|
|
222
|
+
const html = await Bun.file(resolve(htmlPath)).text();
|
|
223
|
+
const written = await ejectSource(html, resolve(outDir), { force: !!args.force });
|
|
224
|
+
console.log(`\n✓ ejected ${written.length} files → ${outDir}\n`);
|
|
225
|
+
for (const f of written) console.log(` ${f}`);
|
|
226
|
+
// Rebuilding runs the deck's own build-time code (macros/build plugins);
|
|
227
|
+
// `--ignore-scripts` only blocks npm lifecycle scripts, not that — so the real
|
|
228
|
+
// control is to rebuild only decks you trust. `liebstoeckel build` confirms it once.
|
|
229
|
+
console.log(`\n rebuild (runs the deck's code — only rebuild decks you trust):`);
|
|
230
|
+
console.log(` cd ${outDir} && bun install --ignore-scripts && liebstoeckel build`);
|
|
231
|
+
// An ejected deck isn't trusted yet, so the first rebuild asks you to confirm. Frame
|
|
232
|
+
// that as a human decision — deliberately NOT "just add --trust", which trains an agent
|
|
233
|
+
// to self-approve a deck it didn't write (the exact bypass the gate exists to prevent).
|
|
234
|
+
console.log(` the first rebuild asks you to confirm trust — that's a human decision, not one for an agent.\n`);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.error(`✕ ${(e as Error).message}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
export const packCommand = defineCommand({
|
|
243
|
+
meta: {
|
|
244
|
+
name: "pack",
|
|
245
|
+
description: "inspect/emit the source a build embeds (default: cwd)",
|
|
246
|
+
},
|
|
247
|
+
args: {
|
|
248
|
+
deck: { type: "positional", required: false, description: "deck directory (default: cwd)", valueHint: "dir" },
|
|
249
|
+
dir: { type: "string", description: "deck directory (alternative to the positional)", valueHint: "deck" },
|
|
250
|
+
out: { type: "string", alias: "o", description: "write the source package to this .tgz", valueHint: "file.tgz" },
|
|
251
|
+
"allow-secret": { type: "boolean", description: "allow packing files outside the deck's `files` allowlist" },
|
|
252
|
+
},
|
|
253
|
+
async run({ args }) {
|
|
254
|
+
const dir = resolve(deckDir(args));
|
|
255
|
+
const out = args.out;
|
|
256
|
+
// A clean "this isn't a deck" beats leaking the underlying `bun pm pack` error
|
|
257
|
+
// ("package.json must have name and version") for the common wrong-directory mistake.
|
|
258
|
+
if (!existsSync(join(dir, "index.html"))) {
|
|
259
|
+
console.error(`✕ no deck here: ${dir}\n run this in a deck directory (one with an index.html), or pass --dir <deck>.`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
const { collectDeckTarball } = await import("@liebstoeckel/engine/build/source-package");
|
|
263
|
+
try {
|
|
264
|
+
const { gzip, files } = await collectDeckTarball(dir, { allowSecret: !!args.allowSecret });
|
|
265
|
+
if (out) {
|
|
266
|
+
// pack's native gzip, `bun add ./<file>.tgz`-installable (zstd is embed-only).
|
|
267
|
+
await Bun.write(resolve(out), gzip);
|
|
268
|
+
console.log(`\n✓ wrote ${files.length}-file source package → ${out} (gzip; bun add-compatible)\n`);
|
|
269
|
+
} else {
|
|
270
|
+
console.log(`\nsource package (${files.length} files), what a build would embed:\n`);
|
|
271
|
+
}
|
|
272
|
+
for (const f of files) console.log(` ${f}`);
|
|
273
|
+
console.log();
|
|
274
|
+
} catch (e) {
|
|
275
|
+
console.error(`✕ ${(e as Error).message}`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
export const licensesCommand = defineCommand({
|
|
282
|
+
meta: {
|
|
283
|
+
name: "licenses",
|
|
284
|
+
description: "report third-party licenses bundled into a deck",
|
|
285
|
+
},
|
|
286
|
+
args: {
|
|
287
|
+
deck: { type: "positional", required: false, description: "built deck .html or deck dir (default: cwd)", valueHint: "deck.html|dir" },
|
|
288
|
+
dir: { type: "string", description: "deck directory (alternative to the positional)", valueHint: "deck" },
|
|
289
|
+
json: { type: "boolean", description: "machine-readable JSON output (default when piped)" },
|
|
290
|
+
check: { type: "boolean", description: "fail on non-standard licenses (needs the deck source dir)" },
|
|
291
|
+
trust: {
|
|
292
|
+
type: "boolean",
|
|
293
|
+
description: "trust this deck's build-time code (recomputing from source runs it; remembered)",
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
async run({ args }) {
|
|
297
|
+
const json = !!args.json || !process.stdout.isTTY;
|
|
298
|
+
const check = !!args.check;
|
|
299
|
+
const dir = deckDir(args);
|
|
300
|
+
|
|
301
|
+
// A built deck.html already carries its notices, print the embedded block
|
|
302
|
+
// (no rebuild). `--check` is not meaningful here: the block is rendered text, not the
|
|
303
|
+
// structured report, so license gating needs the deck source instead.
|
|
304
|
+
if (looksLikeDeck(dir) && /\.html?$/i.test(dir)) {
|
|
305
|
+
if (check) {
|
|
306
|
+
console.error(`✕ --check needs the deck source directory (it recomputes the bundle); a built .html carries only the rendered notices.\n try: liebstoeckel licenses <deck-dir> --check`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
if (!existsSync(resolve(dir))) {
|
|
310
|
+
console.error(`✕ no such deck file: ${dir}`);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
const { extractLicenses } = await import("@liebstoeckel/engine/build/licenses");
|
|
314
|
+
const notices = extractLicenses(await Bun.file(resolve(dir)).text());
|
|
315
|
+
if (!notices) {
|
|
316
|
+
console.error(`✕ no embedded license notices in ${dir} (built with --no-inline-licenses or an older build)`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
if (json) console.log(JSON.stringify({ source: "embedded", notices }, null, 2));
|
|
320
|
+
else console.log(notices);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Otherwise resolve the deck dir and compute the report from its real module graph —
|
|
325
|
+
// this runs the deck's build-time code, so it's behind the same trust gate as `build`.
|
|
326
|
+
await gateBuildTrust(dir, args.trust);
|
|
327
|
+
const prev = process.cwd();
|
|
328
|
+
process.chdir(resolve(dir));
|
|
329
|
+
try {
|
|
330
|
+
const { collectDeckLicenses } = await import("@liebstoeckel/engine/build");
|
|
331
|
+
const report = await collectDeckLicenses({ entry: "./index.html" });
|
|
332
|
+
const ok = report.flagged.length === 0;
|
|
333
|
+
if (json) {
|
|
334
|
+
console.log(JSON.stringify({ ok, ...report }, null, 2));
|
|
335
|
+
} else {
|
|
336
|
+
console.log(`\nthird-party licenses bundled into this deck (${report.packages.length} packages):\n`);
|
|
337
|
+
for (const p of report.packages) {
|
|
338
|
+
const mark = report.flagged.some((f) => f.name === p.name && f.version === p.version) ? " ⚠" : "";
|
|
339
|
+
console.log(` ${`${p.name}@${p.version}`.padEnd(40)} ${p.license}${mark}`);
|
|
340
|
+
}
|
|
341
|
+
if (report.firstParty.length) console.log(`\n + ${report.firstParty.length} liebstoeckel package(s), MPL-2.0`);
|
|
342
|
+
if (!ok) {
|
|
343
|
+
console.error(`\n⚠ ${report.flagged.length} non-standard license(s), review before distributing:`);
|
|
344
|
+
for (const f of report.flagged) console.error(` ${f.name}@${f.version} ${f.license}`);
|
|
345
|
+
} else {
|
|
346
|
+
console.log(`\n✓ all bundled licenses are standard permissive / embeddable.`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (check && !ok) process.exit(1);
|
|
350
|
+
} finally {
|
|
351
|
+
process.chdir(prev);
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
});
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { looksLikeDeck } from "./targeting";
|
|
4
|
+
|
|
5
|
+
// The umbrella dispatches in-process to one citty command per subcommand (ADR 0050:
|
|
6
|
+
// uniform deck targeting, ADR 0045: agent-readable surface). Heavy command modules
|
|
7
|
+
// are imported lazily, a subCommand is a `() => import(...)` thunk, so e.g. `build`
|
|
8
|
+
// never loads the live server and `live` never loads the bundler until invoked.
|
|
9
|
+
const rootCommand = defineCommand({
|
|
10
|
+
meta,
|
|
11
|
+
subCommands: {
|
|
12
|
+
new: () => import("./new").then((m) => m.newCommand),
|
|
13
|
+
add: () => import("./add").then((m) => m.addCommand),
|
|
14
|
+
registry: () => import("./registry").then((m) => m.registryCommand),
|
|
15
|
+
build: () => import("./build").then((m) => m.buildCommand),
|
|
16
|
+
eject: () => import("./build").then((m) => m.ejectCommand),
|
|
17
|
+
pack: () => import("./build").then((m) => m.packCommand),
|
|
18
|
+
licenses: () => import("./build").then((m) => m.licensesCommand),
|
|
19
|
+
live: () => import("@liebstoeckel/live-server/cli").then((m) => m.liveCommand),
|
|
20
|
+
relay: () => import("@liebstoeckel/present-relay/cli").then((m) => m.relayCommand),
|
|
21
|
+
thumbs: () => import("@liebstoeckel/thumbnails/cli").then((m) => m.thumbsCommand),
|
|
22
|
+
export: () => import("@liebstoeckel/thumbnails/cli").then((m) => m.exportCommand),
|
|
23
|
+
skill: () => import("./skill").then((m) => m.skillCommand),
|
|
24
|
+
// cloud (coming soon, the hosted service is not generally available yet):
|
|
25
|
+
login: () => import("./cloud").then((m) => m.loginCommand),
|
|
26
|
+
push: () => import("./cloud").then((m) => m.pushCommand),
|
|
27
|
+
orgs: () => import("./cloud").then((m) => m.orgsCommand),
|
|
28
|
+
decks: () => import("./cloud").then((m) => m.decksCommand),
|
|
29
|
+
brand: () => import("./cloud").then((m) => m.brandCommand),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** Root meta with the live CLI version, so `liebstoeckel --version` and the usage
|
|
34
|
+
* header report it. A function so the version (read from package.json) is resolved
|
|
35
|
+
* only when help/version is rendered. */
|
|
36
|
+
async function meta() {
|
|
37
|
+
const { cliVersion } = await import("./skill");
|
|
38
|
+
return {
|
|
39
|
+
name: "liebstoeckel",
|
|
40
|
+
version: await cliVersion(),
|
|
41
|
+
description: "code-first presentations, author decks in MDX + TSX, build one self-contained .html (alias: lst)",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The subcommand names that win over the `liebstoeckel <deck>` → `live` shorthand. */
|
|
46
|
+
const KNOWN_COMMANDS = new Set(Object.keys((rootCommand.subCommands as Record<string, unknown>) ?? {}));
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const argv = process.argv.slice(2);
|
|
50
|
+
|
|
51
|
+
// Best-effort reminders (stderr-only; off for --json/pipes/CI, see update.ts):
|
|
52
|
+
// a cached "new CLI version" note and a "deck skill older than the CLI" note.
|
|
53
|
+
try {
|
|
54
|
+
const { updateReminder, skillReminder } = await import("./update");
|
|
55
|
+
const dirIdx = argv.indexOf("--dir");
|
|
56
|
+
const deckDir = dirIdx >= 0 ? argv[dirIdx + 1] ?? "." : ".";
|
|
57
|
+
await updateReminder(argv);
|
|
58
|
+
await skillReminder(deckDir, argv);
|
|
59
|
+
} catch {
|
|
60
|
+
// reminders must never break a command
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Shorthand: `liebstoeckel <deck>` → `liebstoeckel live <deck>`. citty's subcommand
|
|
64
|
+
// router throws on an unknown leading positional, so resolve the shorthand here by
|
|
65
|
+
// injecting `live` before handing off (an unknown non-deck token still falls through
|
|
66
|
+
// to citty's "Unknown command" usage error).
|
|
67
|
+
const firstPositional = argv.find((a) => !a.startsWith("-"));
|
|
68
|
+
let rawArgs = argv;
|
|
69
|
+
if (firstPositional && !KNOWN_COMMANDS.has(firstPositional) && looksLikeDeck(firstPositional)) {
|
|
70
|
+
rawArgs = ["live", ...argv];
|
|
71
|
+
}
|
|
72
|
+
// Bare invocation → show the command surface (citty would otherwise error).
|
|
73
|
+
if (rawArgs.length === 0) rawArgs = ["--help"];
|
|
74
|
+
|
|
75
|
+
await runMain(rootCommand, { rawArgs });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (import.meta.main) void main();
|