@liustack/pptfast 0.1.0 → 0.4.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/README.md +114 -11
- package/README.zh-CN.md +108 -11
- package/dist/chunk-4W2YZPJJ.js +564 -0
- package/dist/chunk-4W2YZPJJ.js.map +1 -0
- package/dist/chunk-7N4HGSMW.js +19013 -0
- package/dist/chunk-7N4HGSMW.js.map +1 -0
- package/dist/chunk-HFRNKYZ6.js +54 -0
- package/dist/chunk-HFRNKYZ6.js.map +1 -0
- package/dist/chunk-L524YK63.js +19 -0
- package/dist/chunk-L524YK63.js.map +1 -0
- package/dist/cli.js +1094 -44
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1716 -62
- package/dist/index.js +66 -6
- package/dist/node.d.ts +3 -2
- package/dist/node.js +2 -2
- package/dist/pixel-audit-4DXKLFBV.js +176 -0
- package/dist/pixel-audit-4DXKLFBV.js.map +1 -0
- package/dist/registry-V079Jkry.d.ts +40 -0
- package/package.json +6 -4
- package/dist/chunk-4WISUDXE.js +0 -34
- package/dist/chunk-4WISUDXE.js.map +0 -1
- package/dist/chunk-DVMJWFDL.js +0 -14465
- package/dist/chunk-DVMJWFDL.js.map +0 -1
- package/dist/chunk-G76EVNVT.js +0 -14
- package/dist/chunk-G76EVNVT.js.map +0 -1
- package/dist/registry-CYKxZ0-U.d.ts +0 -11
package/dist/cli.js
CHANGED
|
@@ -1,58 +1,153 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
PptxIRV3Schema,
|
|
4
4
|
VERSION,
|
|
5
|
+
assembleDeck,
|
|
6
|
+
disassembleDeck,
|
|
7
|
+
formatInvalidSpecError,
|
|
8
|
+
migrateDeckPlanToSpec,
|
|
9
|
+
migrateIrV3ToV4,
|
|
10
|
+
resolveSpecThemeId,
|
|
11
|
+
specJsonSchema,
|
|
12
|
+
validateSpec
|
|
13
|
+
} from "./chunk-4W2YZPJJ.js";
|
|
14
|
+
import {
|
|
15
|
+
installNodePlatform
|
|
16
|
+
} from "./chunk-HFRNKYZ6.js";
|
|
17
|
+
import {
|
|
18
|
+
AUDIENCE_VALUES,
|
|
19
|
+
NARRATIVE_PRESETS,
|
|
20
|
+
PACING_BUDGETS,
|
|
21
|
+
PptfastError,
|
|
22
|
+
STRATEGY_DEFINITIONS,
|
|
23
|
+
StyleOverrideSchema,
|
|
24
|
+
auditDeck,
|
|
5
25
|
formatIssues,
|
|
6
26
|
generatePptx,
|
|
27
|
+
getInstalledThemeIds,
|
|
7
28
|
irJsonSchema,
|
|
8
29
|
listThemes,
|
|
9
30
|
renderSlideSvg,
|
|
31
|
+
resolveNarrative,
|
|
32
|
+
styleJsonSchema,
|
|
10
33
|
validateIr
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import {
|
|
13
|
-
installNodePlatform
|
|
14
|
-
} from "./chunk-4WISUDXE.js";
|
|
34
|
+
} from "./chunk-7N4HGSMW.js";
|
|
15
35
|
import {
|
|
16
36
|
getPlatform
|
|
17
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-L524YK63.js";
|
|
18
38
|
|
|
19
39
|
// src/cli.ts
|
|
20
40
|
import { Command } from "commander";
|
|
21
41
|
|
|
22
42
|
// src/cli/commands.ts
|
|
23
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
24
|
-
import { dirname, join, resolve as
|
|
43
|
+
import { mkdir as mkdir2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
44
|
+
import { dirname as dirname2, join as join4, relative as relative2, resolve as resolve5 } from "path";
|
|
25
45
|
|
|
26
|
-
// src/cli/
|
|
46
|
+
// src/cli/config.ts
|
|
27
47
|
import { readFile } from "fs/promises";
|
|
28
|
-
import {
|
|
48
|
+
import { dirname, join as join2, resolve as resolve2 } from "path";
|
|
49
|
+
import { z } from "zod";
|
|
50
|
+
|
|
51
|
+
// src/cli/home.ts
|
|
52
|
+
import { homedir } from "os";
|
|
53
|
+
import { join, resolve } from "path";
|
|
54
|
+
function pptfastHome() {
|
|
55
|
+
return process.env.PPTFAST_HOME ?? join(homedir(), ".pptfast");
|
|
56
|
+
}
|
|
57
|
+
function decksRoot(config) {
|
|
58
|
+
return resolve(pptfastHome(), config?.decksDir ?? "decks");
|
|
59
|
+
}
|
|
60
|
+
function userConfigPath() {
|
|
61
|
+
return join(pptfastHome(), "config.json");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/cli/config.ts
|
|
65
|
+
var ConfigSchema = z.object({
|
|
66
|
+
theme: z.string().optional(),
|
|
67
|
+
style: StyleOverrideSchema.optional(),
|
|
68
|
+
decksDir: z.string().optional()
|
|
69
|
+
}).strict();
|
|
70
|
+
var UserConfigSchema = z.object({
|
|
71
|
+
theme: z.string().optional(),
|
|
72
|
+
style: StyleOverrideSchema.optional(),
|
|
73
|
+
decksDir: z.string().optional()
|
|
74
|
+
}).strict();
|
|
75
|
+
var CONFIG_FILENAME = "pptfast.config.json";
|
|
76
|
+
async function readConfigFile(path, schema) {
|
|
77
|
+
let text;
|
|
78
|
+
try {
|
|
79
|
+
text = await readFile(path, "utf8");
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = JSON.parse(text);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
throw new PptfastError(`${path} is not valid JSON: ${e.message}`);
|
|
88
|
+
}
|
|
89
|
+
const r = schema.safeParse(raw);
|
|
90
|
+
if (!r.success) {
|
|
91
|
+
const detail = r.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
92
|
+
throw new PptfastError(`invalid ${path}:
|
|
93
|
+
${detail}`);
|
|
94
|
+
}
|
|
95
|
+
return { path, config: r.data };
|
|
96
|
+
}
|
|
97
|
+
async function findConfig(startDir) {
|
|
98
|
+
let dir = resolve2(startDir);
|
|
99
|
+
for (; ; ) {
|
|
100
|
+
const hit = await readConfigFile(join2(dir, CONFIG_FILENAME), ConfigSchema);
|
|
101
|
+
if (hit) return hit;
|
|
102
|
+
const parent = dirname(dir);
|
|
103
|
+
if (parent === dir) return null;
|
|
104
|
+
dir = parent;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function findUserConfig() {
|
|
108
|
+
return readConfigFile(userConfigPath(), UserConfigSchema);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/cli/deck-dir.ts
|
|
112
|
+
import { copyFile, mkdir, readFile as readFile3, readdir, stat, writeFile } from "fs/promises";
|
|
113
|
+
import { basename, extname as extname2, isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve4 } from "path";
|
|
114
|
+
|
|
115
|
+
// src/cli/load-ir.ts
|
|
116
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
117
|
+
import { extname, isAbsolute, resolve as resolve3 } from "path";
|
|
29
118
|
var MIME_BY_EXT = {
|
|
30
119
|
".png": "image/png",
|
|
31
120
|
".jpg": "image/jpeg",
|
|
32
121
|
".jpeg": "image/jpeg",
|
|
33
122
|
".gif": "image/gif"
|
|
34
123
|
};
|
|
35
|
-
|
|
124
|
+
var EXT_BY_MIME = {
|
|
125
|
+
"image/png": ".png",
|
|
126
|
+
"image/jpeg": ".jpg",
|
|
127
|
+
"image/gif": ".gif",
|
|
128
|
+
"image/webp": ".webp"
|
|
129
|
+
};
|
|
130
|
+
async function loadIrFile(irPath, kind = "IR") {
|
|
36
131
|
let text;
|
|
37
132
|
try {
|
|
38
|
-
text = await
|
|
133
|
+
text = await readFile2(irPath, "utf8");
|
|
39
134
|
} catch {
|
|
40
|
-
throw new PptfastError(`cannot read
|
|
135
|
+
throw new PptfastError(`cannot read ${kind} file: ${irPath}`);
|
|
41
136
|
}
|
|
42
137
|
try {
|
|
43
138
|
return JSON.parse(text);
|
|
44
139
|
} catch (e) {
|
|
45
|
-
throw new PptfastError(`${irPath} is not valid JSON: ${e.message}`);
|
|
140
|
+
throw new PptfastError(`${kind} file ${irPath} is not valid JSON: ${e.message}`);
|
|
46
141
|
}
|
|
47
142
|
}
|
|
48
143
|
async function resolveLocalAssets(ir, baseDir) {
|
|
49
144
|
for (const [name, asset] of Object.entries(ir.assets.images)) {
|
|
50
145
|
const src = asset.src;
|
|
51
146
|
if (src.startsWith("data:") || /^https?:\/\//.test(src)) continue;
|
|
52
|
-
const abs = isAbsolute(src) ? src :
|
|
147
|
+
const abs = isAbsolute(src) ? src : resolve3(baseDir, src);
|
|
53
148
|
let bytes;
|
|
54
149
|
try {
|
|
55
|
-
bytes = await
|
|
150
|
+
bytes = await readFile2(abs);
|
|
56
151
|
} catch {
|
|
57
152
|
throw new PptfastError(`asset "${name}": cannot read image file ${abs} (from src "${src}")`);
|
|
58
153
|
}
|
|
@@ -71,53 +166,924 @@ async function resolveLocalAssets(ir, baseDir) {
|
|
|
71
166
|
}
|
|
72
167
|
}
|
|
73
168
|
|
|
169
|
+
// src/cli/deck-dir.ts
|
|
170
|
+
var PLAN_FILENAME = "deck.plan.json";
|
|
171
|
+
var SPEC_FILENAME = "deck.spec.json";
|
|
172
|
+
var PAGES_DIRNAME = "pages";
|
|
173
|
+
var ASSETS_DIRNAME = "assets";
|
|
174
|
+
function assertSafeFileSegment(id, context) {
|
|
175
|
+
const safeBase = resolve4("/pptfast-safe-base");
|
|
176
|
+
const rel = relative(safeBase, resolve4(safeBase, id));
|
|
177
|
+
const safe = !isAbsolute2(id) && !id.includes("/") && !id.includes("\\") && id !== ".." && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
178
|
+
if (!safe) {
|
|
179
|
+
throw new PptfastError(
|
|
180
|
+
`${context} "${id}" is not a safe file name \u2014 ids used as page/asset file names must not contain path separators or ".."`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function isDeckDirectory(path) {
|
|
185
|
+
try {
|
|
186
|
+
return (await stat(path)).isDirectory();
|
|
187
|
+
} catch (e) {
|
|
188
|
+
if (e.code === "ENOENT") return false;
|
|
189
|
+
throw new PptfastError(`cannot check ${path}: ${e.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function pathExists(path) {
|
|
193
|
+
try {
|
|
194
|
+
await stat(path);
|
|
195
|
+
return true;
|
|
196
|
+
} catch (e) {
|
|
197
|
+
if (e.code === "ENOENT") return false;
|
|
198
|
+
throw new PptfastError(`cannot check path ${path}: ${e.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function resolveDeckTarget(arg, config, cwd = process.cwd()) {
|
|
202
|
+
if (arg.trim() === "") throw new PptfastError("deck target must not be empty");
|
|
203
|
+
if (arg.includes("/") || arg.includes("\\")) return resolve4(cwd, arg);
|
|
204
|
+
const local = resolve4(cwd, arg);
|
|
205
|
+
if (await pathExists(local)) return local;
|
|
206
|
+
const fallback = join3(decksRoot(config), arg);
|
|
207
|
+
return await pathExists(fallback) ? fallback : local;
|
|
208
|
+
}
|
|
209
|
+
function expectedLayoutHint() {
|
|
210
|
+
const rows = [
|
|
211
|
+
[SPEC_FILENAME, "the locked spec (see `pptfast spec validate`)"],
|
|
212
|
+
[`${PAGES_DIRNAME}/<page-id>.json`, "one file per filled page (missing pages become placeholders)"],
|
|
213
|
+
[`${ASSETS_DIRNAME}/`, "optional local images"]
|
|
214
|
+
];
|
|
215
|
+
const width = Math.max(...rows.map(([name]) => name.length)) + 2;
|
|
216
|
+
return rows.map(([name, desc]) => ` ${name.padEnd(width)}${desc}`).join("\n");
|
|
217
|
+
}
|
|
218
|
+
async function readSpecFile(dir) {
|
|
219
|
+
const specPath = join3(dir, SPEC_FILENAME);
|
|
220
|
+
const planPath = join3(dir, PLAN_FILENAME);
|
|
221
|
+
const [specExists, planExists] = await Promise.all([pathExists(specPath), pathExists(planPath)]);
|
|
222
|
+
if (specExists && planExists) {
|
|
223
|
+
throw new PptfastError(
|
|
224
|
+
`both ${SPEC_FILENAME} and ${PLAN_FILENAME} exist in ${dir} \u2014 ambiguous, refusing to guess which one wins. Delete ${PLAN_FILENAME} once you have confirmed ${SPEC_FILENAME} is correct (\`pptfast migrate\` never deletes the source file it read)`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (!specExists && planExists) {
|
|
228
|
+
throw new PptfastError(
|
|
229
|
+
`${dir} has ${PLAN_FILENAME} but no ${SPEC_FILENAME} \u2014 deck project directories now use ${SPEC_FILENAME}. Run \`pptfast migrate ${dir} -o ${dir}\` to convert it`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
let text;
|
|
233
|
+
try {
|
|
234
|
+
text = await readFile3(specPath, "utf8");
|
|
235
|
+
} catch (e) {
|
|
236
|
+
if (e.code === "ENOENT") {
|
|
237
|
+
throw new PptfastError(
|
|
238
|
+
`no ${SPEC_FILENAME} in ${dir} \u2014 expected a deck project directory:
|
|
239
|
+
${expectedLayoutHint()}`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
throw new PptfastError(`cannot read spec file: ${specPath}`);
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
return JSON.parse(text);
|
|
246
|
+
} catch (e) {
|
|
247
|
+
throw new PptfastError(`spec file ${specPath} is not valid JSON: ${e.message}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function readPages(dir) {
|
|
251
|
+
const pagesDir = join3(dir, PAGES_DIRNAME);
|
|
252
|
+
let entries;
|
|
253
|
+
try {
|
|
254
|
+
entries = (await readdir(pagesDir, { withFileTypes: true })).filter((entry) => entry.isFile() && extname2(entry.name) === ".json").map((entry) => entry.name);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
if (e.code === "ENOENT") return {};
|
|
257
|
+
throw new PptfastError(`cannot read ${PAGES_DIRNAME}/ directory ${pagesDir}: ${e.message}`);
|
|
258
|
+
}
|
|
259
|
+
const pages = {};
|
|
260
|
+
await Promise.all(
|
|
261
|
+
entries.map(async (entry) => {
|
|
262
|
+
const id = basename(entry, ".json");
|
|
263
|
+
pages[id] = await loadIrFile(join3(pagesDir, entry), `page "${id}"`);
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
return pages;
|
|
267
|
+
}
|
|
268
|
+
async function scanAssets(dir) {
|
|
269
|
+
const assetsDir = join3(dir, ASSETS_DIRNAME);
|
|
270
|
+
let entries;
|
|
271
|
+
try {
|
|
272
|
+
entries = (await readdir(assetsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
273
|
+
} catch (e) {
|
|
274
|
+
if (e.code === "ENOENT") return {};
|
|
275
|
+
throw new PptfastError(`cannot read ${ASSETS_DIRNAME}/ directory ${assetsDir}: ${e.message}`);
|
|
276
|
+
}
|
|
277
|
+
const images = {};
|
|
278
|
+
const sourceFile = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
const id = basename(entry, extname2(entry));
|
|
281
|
+
const previous = sourceFile.get(id);
|
|
282
|
+
if (previous !== void 0) {
|
|
283
|
+
throw new PptfastError(
|
|
284
|
+
`${ASSETS_DIRNAME}/${previous} and ${ASSETS_DIRNAME}/${entry} both register image id "${id}" \u2014 rename one of the files`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
sourceFile.set(id, entry);
|
|
288
|
+
images[id] = { src: `${ASSETS_DIRNAME}/${entry}` };
|
|
289
|
+
}
|
|
290
|
+
return images;
|
|
291
|
+
}
|
|
292
|
+
async function readDeckDir(dir) {
|
|
293
|
+
const deckDir = resolve4(dir);
|
|
294
|
+
const spec2 = await readSpecFile(deckDir);
|
|
295
|
+
const pages = await readPages(deckDir);
|
|
296
|
+
const { ir, generatedSeed, materializedLayoutCount } = assembleDeck(spec2, pages);
|
|
297
|
+
const images = await scanAssets(deckDir);
|
|
298
|
+
const merged = { ...ir, assets: { images: { ...ir.assets.images, ...images } } };
|
|
299
|
+
return { ir: merged, generatedSeed, materializedLayoutCount, deckDir };
|
|
300
|
+
}
|
|
301
|
+
async function writeDeckAssets(images, outDir, sourceBaseDir) {
|
|
302
|
+
const entries = Object.entries(images);
|
|
303
|
+
const assetsDir = join3(outDir, ASSETS_DIRNAME);
|
|
304
|
+
if (entries.length === 0) return { count: 0, assetsDir };
|
|
305
|
+
await mkdir(assetsDir, { recursive: true });
|
|
306
|
+
await Promise.all(entries.map(([id, asset]) => writeOneAsset(id, asset.src, assetsDir, sourceBaseDir)));
|
|
307
|
+
return { count: entries.length, assetsDir };
|
|
308
|
+
}
|
|
309
|
+
var DATA_URI_RE = /^data:([^;,]+);base64,(.*)$/s;
|
|
310
|
+
async function writeOneAsset(id, src, assetsDir, sourceBaseDir) {
|
|
311
|
+
assertSafeFileSegment(id, "asset id");
|
|
312
|
+
if (src.startsWith("data:")) {
|
|
313
|
+
const match = DATA_URI_RE.exec(src);
|
|
314
|
+
if (!match) {
|
|
315
|
+
throw new PptfastError(`asset "${id}": only base64-encoded data URIs can be disassembled (malformed data URI)`);
|
|
316
|
+
}
|
|
317
|
+
const mime = match[1];
|
|
318
|
+
const payload = match[2];
|
|
319
|
+
const ext = EXT_BY_MIME[mime];
|
|
320
|
+
if (!ext) {
|
|
321
|
+
throw new PptfastError(
|
|
322
|
+
`asset "${id}": cannot disassemble a data URI with mime "${mime}" \u2014 expected one of ${Object.keys(EXT_BY_MIME).join(", ")}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
await writeFile(join3(assetsDir, `${id}${ext}`), Buffer.from(payload, "base64"));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (/^https?:\/\//.test(src)) {
|
|
329
|
+
throw new PptfastError(
|
|
330
|
+
`asset "${id}": URL assets cannot be disassembled into a deck directory \u2014 inline it as a data URI or download it first`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const abs = isAbsolute2(src) ? src : resolve4(sourceBaseDir, src);
|
|
334
|
+
try {
|
|
335
|
+
await copyFile(abs, join3(assetsDir, `${id}${extname2(abs)}`));
|
|
336
|
+
} catch {
|
|
337
|
+
throw new PptfastError(`asset "${id}": cannot read source image ${abs} (from src "${src}") \u2014 cannot disassemble`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/cli/preview-html.ts
|
|
342
|
+
function escapeHtml(s) {
|
|
343
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
344
|
+
}
|
|
345
|
+
function embedJson(value) {
|
|
346
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
347
|
+
}
|
|
348
|
+
function findingBadge(count, className) {
|
|
349
|
+
if (count === 0) return "";
|
|
350
|
+
return `<div class="${className}" aria-hidden="true">${count}</div>`;
|
|
351
|
+
}
|
|
352
|
+
function slideNode(slide, findingCount) {
|
|
353
|
+
const idAttr = slide.id !== void 0 ? ` data-id="${escapeHtml(slide.id)}"` : "";
|
|
354
|
+
const badge = slide.placeholder ? `<div class="pf-badge" aria-hidden="true">unfilled</div>` : "";
|
|
355
|
+
const fBadge = findingBadge(findingCount, "pf-finding-badge");
|
|
356
|
+
return `<div class="pf-slide" id="pf-slide-${slide.index}" data-index="${slide.index}"${idAttr}>${badge}${fBadge}${slide.svg}</div>`;
|
|
357
|
+
}
|
|
358
|
+
function thumbDescription(slide) {
|
|
359
|
+
const parts = [`slide ${slide.index + 1}`, slide.type];
|
|
360
|
+
if (slide.id !== void 0) parts.push(slide.id);
|
|
361
|
+
if (slide.placeholder) parts.push("unfilled");
|
|
362
|
+
return escapeHtml(parts.join(" \xB7 "));
|
|
363
|
+
}
|
|
364
|
+
function positionLabel(slide) {
|
|
365
|
+
const idPart = slide.id !== void 0 ? ` \xB7 ${slide.id}` : "";
|
|
366
|
+
return escapeHtml(`${slide.index + 1}${idPart}`);
|
|
367
|
+
}
|
|
368
|
+
function counterText(slide, total) {
|
|
369
|
+
const idPart = slide.id !== void 0 ? ` \xB7 ${slide.id}` : "";
|
|
370
|
+
return escapeHtml(`${slide.index + 1} / ${total}${idPart}`);
|
|
371
|
+
}
|
|
372
|
+
function thumbButton(slide, isActive, slotContent, findingCount) {
|
|
373
|
+
const description = thumbDescription(slide);
|
|
374
|
+
const badge = slide.placeholder ? `<span class="pf-thumb-badge" aria-hidden="true">unfilled</span>` : "";
|
|
375
|
+
const fBadge = findingBadge(findingCount, "pf-thumb-finding-badge");
|
|
376
|
+
return `<button type="button" class="pf-thumb${isActive ? " pf-thumb-active" : ""}" id="pf-thumb-${slide.index}" data-index="${slide.index}" title="${description}" aria-label="${description}"><span class="pf-thumb-slot" id="pf-slot-${slide.index}">${slotContent}</span><span class="pf-thumb-label">${positionLabel(slide)}</span>${badge}${fBadge}</button>`;
|
|
377
|
+
}
|
|
378
|
+
function findingPanelEntry(f) {
|
|
379
|
+
const idPart = f.slideId !== void 0 ? ` \xB7 ${escapeHtml(f.slideId)}` : "";
|
|
380
|
+
return `<button type="button" class="pf-finding" data-page-index="${f.page - 1}"><span class="pf-finding-loc">page ${f.page}${idPart}</span><span class="pf-finding-code">[${escapeHtml(f.code)}]</span> <span class="pf-finding-msg">${escapeHtml(f.message)}</span></button>`;
|
|
381
|
+
}
|
|
382
|
+
var CSS = `
|
|
383
|
+
:root{color-scheme:light}
|
|
384
|
+
*{box-sizing:border-box}
|
|
385
|
+
html,body{height:100%;margin:0}
|
|
386
|
+
body{display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;background:#f4f4f4;color:#1a1a1a}
|
|
387
|
+
header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#fff;border-bottom:1px solid #ddd;font-size:14px;flex:0 0 auto}
|
|
388
|
+
#pf-title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
389
|
+
#pf-counter{font-variant-numeric:tabular-nums;color:#555;white-space:nowrap}
|
|
390
|
+
#pf-audit-note{color:#b45309;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
391
|
+
#pf-export-btn{font:inherit;font-size:13px;padding:6px 10px;border:1px solid #2563eb;background:#2563eb;color:#fff;border-radius:4px;cursor:pointer;white-space:nowrap;flex:0 0 auto}
|
|
392
|
+
#pf-export-btn:hover{background:#1d4ed8}
|
|
393
|
+
#pf-stage-wrap{flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:center;gap:16px;padding:16px}
|
|
394
|
+
#pf-stage{position:relative;background:#000;box-shadow:0 2px 16px rgba(0,0,0,.15);aspect-ratio:16/9;width:min(100%,calc((100vh - 190px) * 16 / 9));max-height:100%}
|
|
395
|
+
#pf-stage,.pf-thumb-slot{position:relative}
|
|
396
|
+
.pf-slide{position:absolute;inset:0}
|
|
397
|
+
.pf-slide svg{display:block;width:100%;height:100%}
|
|
398
|
+
#pf-side{flex:0 0 260px;align-self:stretch;overflow-y:auto;background:#fff;border:1px solid #ddd;border-radius:6px;padding:12px;font-size:13px}
|
|
399
|
+
#pf-side h2{margin:0 0 8px;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:#666}
|
|
400
|
+
#pf-side section+section{margin-top:16px;padding-top:16px;border-top:1px solid #eee}
|
|
401
|
+
#pf-audit-checks{margin:0 0 12px;font-size:12px;color:#555}
|
|
402
|
+
.pf-finding{display:block;width:100%;text-align:left;background:#fff;border:1px solid #eee;border-radius:4px;padding:6px 8px;margin-bottom:6px;cursor:pointer;font:inherit}
|
|
403
|
+
.pf-finding:hover{border-color:#93c5fd}
|
|
404
|
+
.pf-finding-loc{display:block;font-size:11px;color:#888}
|
|
405
|
+
.pf-finding-code{display:inline-block;font-size:11px;font-weight:700;color:#b91c1c}
|
|
406
|
+
.pf-finding-msg{font-size:12px;color:#333}
|
|
407
|
+
#pf-annotate-current-label{font-size:11px;color:#888;margin-bottom:6px}
|
|
408
|
+
#pf-annotate-list{list-style:none;margin:0 0 8px;padding:0}
|
|
409
|
+
.pf-annotate-item{display:flex;justify-content:space-between;gap:6px;align-items:flex-start;padding:4px 0;border-bottom:1px solid #f0f0f0;font-size:12px}
|
|
410
|
+
.pf-annotate-remove{border:none;background:none;color:#999;cursor:pointer;font-size:14px;line-height:1;padding:0 2px}
|
|
411
|
+
.pf-annotate-remove:hover{color:#dc2626}
|
|
412
|
+
#pf-annotate-input{width:100%;box-sizing:border-box;font:inherit;font-size:12px;padding:6px;border:1px solid #ddd;border-radius:4px;resize:vertical}
|
|
413
|
+
#pf-annotate-add{font:inherit;font-size:12px;margin-top:6px;width:100%;padding:6px 10px;border:1px solid #2563eb;background:#2563eb;color:#fff;border-radius:4px;cursor:pointer}
|
|
414
|
+
#pf-annotate-add:hover{background:#1d4ed8}
|
|
415
|
+
#pf-filmstrip{display:flex;gap:8px;padding:10px 16px;overflow-x:auto;background:#fff;border-top:1px solid #ddd;flex:0 0 auto}
|
|
416
|
+
.pf-thumb{flex:0 0 auto;width:160px;padding:0;margin:0;border:2px solid transparent;background:#eee;cursor:pointer;border-radius:6px;overflow:hidden;position:relative;font:inherit;text-align:left}
|
|
417
|
+
.pf-thumb:hover{border-color:#93c5fd}
|
|
418
|
+
.pf-thumb-active,.pf-thumb-active:hover{border-color:#2563eb;background:#dbeafe}
|
|
419
|
+
.pf-thumb-slot{display:block;width:100%;aspect-ratio:16/9;background:#ddd}
|
|
420
|
+
.pf-thumb-label{display:block;font-size:11px;line-height:1.4;padding:3px 6px;color:#444;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
421
|
+
.pf-badge,.pf-thumb-badge{position:absolute;top:4px;right:4px;background:#d97706;color:#fff;font-size:10px;font-weight:700;letter-spacing:.03em;padding:2px 6px;border-radius:3px;text-transform:uppercase;z-index:2;pointer-events:none}
|
|
422
|
+
.pf-finding-badge,.pf-thumb-finding-badge{position:absolute;top:4px;left:4px;background:#dc2626;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:3px;z-index:2;pointer-events:none}
|
|
423
|
+
`.trim();
|
|
424
|
+
var JS = `
|
|
425
|
+
(function () {
|
|
426
|
+
var stage = document.getElementById('pf-stage')
|
|
427
|
+
var counter = document.getElementById('pf-counter')
|
|
428
|
+
var thumbs = Array.prototype.slice.call(document.querySelectorAll('.pf-thumb'))
|
|
429
|
+
var total = thumbs.length
|
|
430
|
+
if (total === 0) return
|
|
431
|
+
var current = parseInt(thumbs[0].getAttribute('data-index'), 10)
|
|
432
|
+
|
|
433
|
+
function slideEl(i) { return document.getElementById('pf-slide-' + i) }
|
|
434
|
+
function slotEl(i) { return document.getElementById('pf-slot-' + i) }
|
|
435
|
+
function thumbEl(i) { return document.getElementById('pf-thumb-' + i) }
|
|
436
|
+
function thumbPos(i) {
|
|
437
|
+
for (var t = 0; t < thumbs.length; t++) {
|
|
438
|
+
if (parseInt(thumbs[t].getAttribute('data-index'), 10) === i) return t
|
|
439
|
+
}
|
|
440
|
+
return -1
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function updateCounter(i) {
|
|
444
|
+
var el = slideEl(i)
|
|
445
|
+
if (!el) return
|
|
446
|
+
var text = (thumbPos(i) + 1) + ' / ' + total
|
|
447
|
+
var id = el.getAttribute('data-id')
|
|
448
|
+
if (id) text += ' \xB7 ' + id
|
|
449
|
+
counter.textContent = text
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function activate(i) {
|
|
453
|
+
if (i === current) return
|
|
454
|
+
var nextSlide = slideEl(i)
|
|
455
|
+
var nextThumb = thumbEl(i)
|
|
456
|
+
if (!nextSlide || !nextThumb) return
|
|
457
|
+
var prevSlide = slideEl(current)
|
|
458
|
+
var prevSlot = slotEl(current)
|
|
459
|
+
if (prevSlide && prevSlot) prevSlot.appendChild(prevSlide)
|
|
460
|
+
var prevThumb = thumbEl(current)
|
|
461
|
+
if (prevThumb) prevThumb.classList.remove('pf-thumb-active')
|
|
462
|
+
stage.appendChild(nextSlide)
|
|
463
|
+
nextThumb.classList.add('pf-thumb-active')
|
|
464
|
+
current = i
|
|
465
|
+
updateCounter(i)
|
|
466
|
+
renderAnnotations()
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
thumbs.forEach(function (t) {
|
|
470
|
+
t.addEventListener('click', function () {
|
|
471
|
+
activate(parseInt(t.getAttribute('data-index'), 10))
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
document.addEventListener('keydown', function (e) {
|
|
476
|
+
var pos = thumbPos(current)
|
|
477
|
+
if (e.key === 'ArrowRight' && pos < total - 1) {
|
|
478
|
+
activate(parseInt(thumbs[pos + 1].getAttribute('data-index'), 10))
|
|
479
|
+
} else if (e.key === 'ArrowLeft' && pos > 0) {
|
|
480
|
+
activate(parseInt(thumbs[pos - 1].getAttribute('data-index'), 10))
|
|
481
|
+
}
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
// ---- audit findings panel: click a finding, jump to its page (same
|
|
485
|
+
// activate() every thumbnail click already uses) ----
|
|
486
|
+
Array.prototype.slice.call(document.querySelectorAll('.pf-finding')).forEach(function (b) {
|
|
487
|
+
b.addEventListener('click', function () {
|
|
488
|
+
activate(parseInt(b.getAttribute('data-page-index'), 10))
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
// ---- annotations: in-memory only, keyed by the slide's 0-based array
|
|
493
|
+
// index (current) \u2014 never by pageId, since an object key coerces every
|
|
494
|
+
// key to a string and would silently collide a numeric page-fallback
|
|
495
|
+
// pageId with a same-looking slide id (or lose the numeric/string type
|
|
496
|
+
// distinction the exported JSON needs); pageIdFor() below resolves the
|
|
497
|
+
// real pageId fresh from the DOM only at render/export time. ----
|
|
498
|
+
var annotations = {}
|
|
499
|
+
var annotateList = document.getElementById('pf-annotate-list')
|
|
500
|
+
var annotateInput = document.getElementById('pf-annotate-input')
|
|
501
|
+
var annotateLabel = document.getElementById('pf-annotate-current-label')
|
|
502
|
+
var annotateAdd = document.getElementById('pf-annotate-add')
|
|
503
|
+
var exportBtn = document.getElementById('pf-export-btn')
|
|
504
|
+
|
|
505
|
+
// Slide id when the active slide has one, else its 1-based page number \u2014
|
|
506
|
+
// mirrors AuditFinding.page's own "1-based page number when there is no
|
|
507
|
+
// slide id" convention (../svg/audit/deck-audit.ts) rather than this
|
|
508
|
+
// file's internal 0-based data-index, so a revision-request.json's
|
|
509
|
+
// pageId lines up with what pptfast audit/validate already print.
|
|
510
|
+
function pageIdFor(i) {
|
|
511
|
+
var el = slideEl(i)
|
|
512
|
+
var id = el ? el.getAttribute('data-id') : null
|
|
513
|
+
return id !== null ? id : thumbPos(i) + 1
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function renderAnnotations() {
|
|
517
|
+
var pid = pageIdFor(current)
|
|
518
|
+
annotateLabel.textContent = 'page ' + (thumbPos(current) + 1) + (typeof pid === 'string' ? ' \xB7 ' + pid : '')
|
|
519
|
+
var list = annotations[current] || []
|
|
520
|
+
annotateList.innerHTML = ''
|
|
521
|
+
list.forEach(function (text, idx) {
|
|
522
|
+
var li = document.createElement('li')
|
|
523
|
+
li.className = 'pf-annotate-item'
|
|
524
|
+
var span = document.createElement('span')
|
|
525
|
+
span.textContent = text
|
|
526
|
+
var rm = document.createElement('button')
|
|
527
|
+
rm.type = 'button'
|
|
528
|
+
rm.className = 'pf-annotate-remove'
|
|
529
|
+
rm.setAttribute('aria-label', 'remove annotation')
|
|
530
|
+
rm.textContent = '\\u00d7'
|
|
531
|
+
rm.addEventListener('click', function () {
|
|
532
|
+
list.splice(idx, 1)
|
|
533
|
+
renderAnnotations()
|
|
534
|
+
})
|
|
535
|
+
li.appendChild(span)
|
|
536
|
+
li.appendChild(rm)
|
|
537
|
+
annotateList.appendChild(li)
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
annotateAdd.addEventListener('click', function () {
|
|
542
|
+
var text = annotateInput.value.trim()
|
|
543
|
+
if (!text) return
|
|
544
|
+
if (!annotations[current]) annotations[current] = []
|
|
545
|
+
annotations[current].push(text)
|
|
546
|
+
annotateInput.value = ''
|
|
547
|
+
renderAnnotations()
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
// "Export revision requests": preview.html stays read-only end to end \u2014
|
|
551
|
+
// this never writes back into the deck itself, only produces a JSON file
|
|
552
|
+
// of requests for an agent/human to route through pages/*.json (see
|
|
553
|
+
// skills/pptfast/SKILL.md's phase-6 revision-request handling). Zero
|
|
554
|
+
// network/storage \u2014 an in-memory Blob + a synthetic <a download> click,
|
|
555
|
+
// the standard browser-only download pattern.
|
|
556
|
+
exportBtn.addEventListener('click', function () {
|
|
557
|
+
var requests = []
|
|
558
|
+
Object.keys(annotations).forEach(function (key) {
|
|
559
|
+
var i = parseInt(key, 10)
|
|
560
|
+
var pid = pageIdFor(i)
|
|
561
|
+
annotations[i].forEach(function (text) {
|
|
562
|
+
requests.push({ pageId: pid, annotation: text, createdAt: new Date().toISOString() })
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
|
+
var deckTitle = document.getElementById('pf-title').textContent
|
|
566
|
+
var payload = { version: '1', deck: deckTitle, requests: requests }
|
|
567
|
+
var blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
|
|
568
|
+
var url = URL.createObjectURL(blob)
|
|
569
|
+
var a = document.createElement('a')
|
|
570
|
+
a.href = url
|
|
571
|
+
a.download = 'revision-request.json'
|
|
572
|
+
document.body.appendChild(a)
|
|
573
|
+
a.click()
|
|
574
|
+
document.body.removeChild(a)
|
|
575
|
+
URL.revokeObjectURL(url)
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
renderAnnotations()
|
|
579
|
+
})()
|
|
580
|
+
`.trim();
|
|
581
|
+
var ANNOTATE_PANEL = `<section id="pf-annotate-panel">
|
|
582
|
+
<h2>Annotations</h2>
|
|
583
|
+
<div id="pf-annotate-current-label"></div>
|
|
584
|
+
<ul id="pf-annotate-list"></ul>
|
|
585
|
+
<textarea id="pf-annotate-input" rows="3" placeholder="Add a note for this page\u2026"></textarea>
|
|
586
|
+
<button type="button" id="pf-annotate-add">Add annotation</button>
|
|
587
|
+
</section>`;
|
|
588
|
+
function buildPreviewHtml(input) {
|
|
589
|
+
const { title, slides, findings = [], auditNote, checks } = input;
|
|
590
|
+
const total = slides.length;
|
|
591
|
+
const escapedTitle = escapeHtml(title);
|
|
592
|
+
const findingsByPage = /* @__PURE__ */ new Map();
|
|
593
|
+
for (const f of findings) {
|
|
594
|
+
const list = findingsByPage.get(f.page);
|
|
595
|
+
if (list) list.push(f);
|
|
596
|
+
else findingsByPage.set(f.page, [f]);
|
|
597
|
+
}
|
|
598
|
+
const countFor = (slide) => findingsByPage.get(slide.index + 1)?.length ?? 0;
|
|
599
|
+
const stageSlide = total > 0 ? slideNode(slides[0], countFor(slides[0])) : "";
|
|
600
|
+
const thumbs = slides.map((s, i) => thumbButton(s, i === 0, i === 0 ? "" : slideNode(s, countFor(s)), countFor(s))).join("");
|
|
601
|
+
const initialCounter = total > 0 ? counterText(slides[0], total) : "0 / 0";
|
|
602
|
+
const auditPanel = findings.length > 0 ? `<section id="pf-audit-panel"><h2>Audit findings (${findings.length})</h2><div id="pf-audit-list">${findings.map(findingPanelEntry).join("")}</div></section>` : "";
|
|
603
|
+
const findingsDataScript = findings.length > 0 ? `<script type="application/json" id="pf-audit-findings">${embedJson(findings)}</script>` : "";
|
|
604
|
+
const auditNoteHtml = auditNote !== void 0 ? `<span id="pf-audit-note">${escapeHtml(auditNote)}</span>` : "";
|
|
605
|
+
const checksLine = checks !== void 0 ? `<div id="pf-audit-checks">audit checks: svg ${checks.svg} \xB7 pixels ${checks.pixels}</div>` : "";
|
|
606
|
+
return `<!doctype html>
|
|
607
|
+
<html lang="en">
|
|
608
|
+
<head>
|
|
609
|
+
<meta charset="utf-8">
|
|
610
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
611
|
+
<title>${escapedTitle} \u2014 pptfast preview</title>
|
|
612
|
+
<style>${CSS}</style>
|
|
613
|
+
</head>
|
|
614
|
+
<body>
|
|
615
|
+
<header>
|
|
616
|
+
<span id="pf-title">${escapedTitle}</span>
|
|
617
|
+
<span id="pf-counter">${initialCounter}</span>
|
|
618
|
+
${auditNoteHtml}
|
|
619
|
+
<button type="button" id="pf-export-btn">Export revision requests</button>
|
|
620
|
+
</header>
|
|
621
|
+
<div id="pf-stage-wrap"><div id="pf-stage">${stageSlide}</div><aside id="pf-side">${checksLine}${auditPanel}${ANNOTATE_PANEL}</aside></div>
|
|
622
|
+
<nav id="pf-filmstrip" aria-label="slides">${thumbs}</nav>
|
|
623
|
+
${findingsDataScript}
|
|
624
|
+
<script>${JS}</script>
|
|
625
|
+
</body>
|
|
626
|
+
</html>
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
|
|
74
630
|
// src/cli/commands.ts
|
|
75
|
-
async function
|
|
76
|
-
const raw = await loadIrFile(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
631
|
+
async function loadStyleFile(path) {
|
|
632
|
+
const raw = await loadIrFile(path);
|
|
633
|
+
const r = StyleOverrideSchema.safeParse(raw);
|
|
634
|
+
if (!r.success) {
|
|
635
|
+
const detail = r.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
636
|
+
throw new PptfastError(`invalid style file ${path}:
|
|
637
|
+
${detail}`);
|
|
638
|
+
}
|
|
639
|
+
return r.data;
|
|
640
|
+
}
|
|
641
|
+
function describeThemeSource(opts, projectHit, userHit) {
|
|
642
|
+
if (opts.theme !== void 0) return "--theme";
|
|
643
|
+
if (projectHit?.config.theme !== void 0) return projectHit.path;
|
|
644
|
+
if (userHit?.config.theme !== void 0) return userHit.path;
|
|
645
|
+
return "the deck's own theme";
|
|
646
|
+
}
|
|
647
|
+
function resolveDecksDirSource(projectHit, userHit) {
|
|
648
|
+
if (projectHit?.config.decksDir !== void 0) {
|
|
649
|
+
return { decksDir: resolve5(dirname2(projectHit.path), projectHit.config.decksDir) };
|
|
650
|
+
}
|
|
651
|
+
return userHit?.config;
|
|
652
|
+
}
|
|
653
|
+
async function applyDeckConfig(raw, opts) {
|
|
654
|
+
if (typeof raw !== "object" || raw === null) return;
|
|
655
|
+
const deck = raw;
|
|
656
|
+
const irTheme = typeof deck.theme === "object" && deck.theme !== null ? deck.theme : {};
|
|
657
|
+
const [projectHit, userHit] = await Promise.all([
|
|
658
|
+
opts.projectHit !== void 0 ? Promise.resolve(opts.projectHit) : findConfig(opts.cwd),
|
|
659
|
+
opts.userHit !== void 0 ? Promise.resolve(opts.userHit) : findUserConfig()
|
|
660
|
+
]);
|
|
661
|
+
const theme = opts.theme ?? projectHit?.config.theme ?? userHit?.config.theme ?? irTheme.id;
|
|
662
|
+
const style = opts.stylePath ? await loadStyleFile(opts.stylePath) : projectHit?.config.style ?? userHit?.config.style ?? irTheme.style;
|
|
663
|
+
if (theme !== void 0) {
|
|
664
|
+
const installedThemeIds = getInstalledThemeIds();
|
|
665
|
+
if (!installedThemeIds.includes(theme)) {
|
|
666
|
+
throw new PptfastError(
|
|
667
|
+
`unknown theme "${theme}" (from ${describeThemeSource(opts, projectHit, userHit)}) \u2014 available: ${installedThemeIds.join(", ")} (see \`pptfast themes\`)`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
80
670
|
}
|
|
671
|
+
if (theme === void 0 && style === void 0) return;
|
|
672
|
+
deck.theme = { ...irTheme, id: theme, ...style !== void 0 ? { style } : {} };
|
|
673
|
+
}
|
|
674
|
+
async function loadDeckTarget(arg, cwd, projectHit, userHit) {
|
|
675
|
+
const target = await resolveDeckTarget(arg, resolveDecksDirSource(projectHit, userHit), cwd);
|
|
676
|
+
if (await isDeckDirectory(target)) {
|
|
677
|
+
const { ir, deckDir } = await readDeckDir(target);
|
|
678
|
+
return { raw: ir, baseDir: deckDir, isDir: true };
|
|
679
|
+
}
|
|
680
|
+
const raw = await loadIrFile(target);
|
|
681
|
+
return { raw, baseDir: dirname2(resolve5(target)), isDir: false };
|
|
682
|
+
}
|
|
683
|
+
async function runRender(irPath, opts) {
|
|
684
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
685
|
+
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
686
|
+
const { raw, baseDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit);
|
|
687
|
+
await applyDeckConfig(raw, { theme: opts.theme, stylePath: opts.stylePath, cwd, projectHit, userHit });
|
|
81
688
|
const v = validateIr(raw);
|
|
82
689
|
if (!v.ok) throw new PptfastError(`invalid IR:
|
|
83
690
|
${formatIssues(v.errors)}`);
|
|
84
|
-
await resolveLocalAssets(v.ir,
|
|
85
|
-
const bytes = await generatePptx(v.ir);
|
|
86
|
-
await
|
|
87
|
-
await
|
|
88
|
-
|
|
691
|
+
await resolveLocalAssets(v.ir, baseDir);
|
|
692
|
+
const bytes = await generatePptx(v.ir, { draft: opts.draft });
|
|
693
|
+
await mkdir2(dirname2(resolve5(opts.output)), { recursive: true });
|
|
694
|
+
await writeFile2(opts.output, bytes);
|
|
695
|
+
const ok = `wrote ${opts.output} (${v.ir.slides.length} slides, ${bytes.length} bytes)`;
|
|
696
|
+
const note = normalizedNote(v.normalized);
|
|
697
|
+
return note ? `${ok}
|
|
698
|
+
${note}` : ok;
|
|
89
699
|
}
|
|
90
|
-
|
|
91
|
-
|
|
700
|
+
function normalizedNote(normalized) {
|
|
701
|
+
if (!normalized || normalized.length === 0) return void 0;
|
|
702
|
+
const n = normalized.length;
|
|
703
|
+
return `note: ${n} field alias${n === 1 ? "" : "es"} normalized
|
|
704
|
+
${normalized.map((line) => ` ${line}`).join("\n")}`;
|
|
705
|
+
}
|
|
706
|
+
function placeholderNote(ir) {
|
|
707
|
+
const placeholders = ir.slides.map((slide, i) => ({ slide, page: i + 1 })).filter(({ slide }) => slide.placeholder);
|
|
708
|
+
if (placeholders.length === 0) return void 0;
|
|
709
|
+
const refs = placeholders.map(({ slide, page }) => slide.id ? `${slide.id} (page ${page})` : `page ${page}`).join(", ");
|
|
710
|
+
return `note: ${placeholders.length} unfilled placeholder page${placeholders.length === 1 ? "" : "s"}: ${refs}`;
|
|
711
|
+
}
|
|
712
|
+
async function runValidate(irPath, cwd = process.cwd()) {
|
|
713
|
+
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
714
|
+
const { raw, isDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit);
|
|
715
|
+
await applyDeckConfig(raw, { cwd, projectHit, userHit });
|
|
92
716
|
const v = validateIr(raw);
|
|
93
717
|
if (!v.ok)
|
|
94
718
|
throw new PptfastError(
|
|
95
719
|
`invalid IR (${v.errors.length} issue${v.errors.length === 1 ? "" : "s"}):
|
|
96
720
|
${formatIssues(v.errors)}`
|
|
97
721
|
);
|
|
98
|
-
|
|
722
|
+
const ok = `OK \u2014 ${v.ir.slides.length} slides, theme "${v.ir.theme.id}"`;
|
|
723
|
+
const notes = [];
|
|
724
|
+
const aliasNote = normalizedNote(v.normalized);
|
|
725
|
+
if (aliasNote) notes.push(aliasNote);
|
|
726
|
+
if (isDir) {
|
|
727
|
+
const note = placeholderNote(v.ir);
|
|
728
|
+
if (note) notes.push(note);
|
|
729
|
+
}
|
|
730
|
+
return notes.length > 0 ? `${ok}
|
|
731
|
+
${notes.join("\n")}` : ok;
|
|
732
|
+
}
|
|
733
|
+
function formatAuditFinding(f) {
|
|
734
|
+
const idSuffix = f.slideId !== void 0 ? ` (${f.slideId})` : "";
|
|
735
|
+
return `page ${f.page}${idSuffix}: [${f.code}] ${f.message}`;
|
|
99
736
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
737
|
+
function formatAuditReport(report, ir) {
|
|
738
|
+
const lines = report.findings.map(formatAuditFinding);
|
|
739
|
+
lines.push(
|
|
740
|
+
`audited ${report.pagesAudited} page${report.pagesAudited === 1 ? "" : "s"}, ${report.pagesSkipped} skipped, ${report.findings.length} finding${report.findings.length === 1 ? "" : "s"}`
|
|
741
|
+
);
|
|
742
|
+
if (report.checks.pixels === "completed") {
|
|
743
|
+
lines.push("pixel-contrast check: completed");
|
|
744
|
+
}
|
|
745
|
+
const note = placeholderNote(ir);
|
|
746
|
+
if (note) lines.push(note);
|
|
747
|
+
return lines.join("\n");
|
|
748
|
+
}
|
|
749
|
+
async function runAudit(target, opts = {}) {
|
|
750
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
751
|
+
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
752
|
+
const { raw, baseDir } = await loadDeckTarget(target, cwd, projectHit, userHit);
|
|
753
|
+
await applyDeckConfig(raw, { cwd, projectHit, userHit });
|
|
754
|
+
const v = validateIr(raw);
|
|
755
|
+
if (!v.ok) {
|
|
756
|
+
throw new PptfastError(
|
|
757
|
+
`invalid IR (${v.errors.length} issue${v.errors.length === 1 ? "" : "s"}):
|
|
758
|
+
${formatIssues(v.errors)}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
await resolveLocalAssets(v.ir, baseDir);
|
|
762
|
+
const report = opts.pixels ? await auditDeck(v.ir, { pixels: true }) : auditDeck(v.ir);
|
|
763
|
+
const hasFindings = report.findings.length > 0;
|
|
764
|
+
const output = opts.json ? JSON.stringify(report, null, 2) : formatAuditReport(report, v.ir);
|
|
765
|
+
return { output, hasFindings };
|
|
766
|
+
}
|
|
767
|
+
async function runSpecValidate(specPath) {
|
|
768
|
+
const raw = await loadIrFile(specPath, "spec");
|
|
769
|
+
const v = validateSpec(raw);
|
|
770
|
+
if (!v.ok) {
|
|
771
|
+
throw new PptfastError(formatInvalidSpecError(v.errors));
|
|
772
|
+
}
|
|
773
|
+
const spec2 = v.spec;
|
|
774
|
+
const axes = resolveNarrative(spec2.narrative);
|
|
775
|
+
return `OK \u2014 ${spec2.pages.length} pages, narrative ${axes.strategy}/${axes.pacing}/${axes.audience}, theme "${resolveSpecThemeId(spec2)}"`;
|
|
776
|
+
}
|
|
777
|
+
function runSchema(mode) {
|
|
778
|
+
const schema = mode === "style" ? styleJsonSchema() : mode === "spec" ? specJsonSchema() : irJsonSchema();
|
|
779
|
+
return JSON.stringify(schema, null, 2);
|
|
102
780
|
}
|
|
103
781
|
function runThemes(asJson) {
|
|
104
782
|
const themes = listThemes();
|
|
105
783
|
if (asJson) return JSON.stringify(themes, null, 2);
|
|
106
784
|
return themes.map((t) => `${t.id.padEnd(12)} ${t.label}`).join("\n");
|
|
107
785
|
}
|
|
108
|
-
|
|
109
|
-
|
|
786
|
+
function runNarratives(asJson) {
|
|
787
|
+
if (asJson) {
|
|
788
|
+
return JSON.stringify(
|
|
789
|
+
{
|
|
790
|
+
presets: NARRATIVE_PRESETS,
|
|
791
|
+
strategies: STRATEGY_DEFINITIONS,
|
|
792
|
+
pacings: PACING_BUDGETS,
|
|
793
|
+
audiences: AUDIENCE_VALUES
|
|
794
|
+
},
|
|
795
|
+
null,
|
|
796
|
+
2
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
const rows = Object.values(NARRATIVE_PRESETS).map((p) => ({
|
|
800
|
+
id: p.id,
|
|
801
|
+
axes: `${p.axes.strategy}/${p.axes.pacing}/${p.axes.audience}`,
|
|
802
|
+
themes: p.themeRecommendations.join(", ")
|
|
803
|
+
}));
|
|
804
|
+
const idWidth = Math.max(...rows.map((r) => r.id.length));
|
|
805
|
+
const axesWidth = Math.max(...rows.map((r) => r.axes.length));
|
|
806
|
+
return rows.map((r) => `${r.id.padEnd(idWidth + 2)}${r.axes.padEnd(axesWidth + 2)}${r.themes}`).join("\n");
|
|
807
|
+
}
|
|
808
|
+
var CONFIG_TEMPLATE = {
|
|
809
|
+
theme: "consulting",
|
|
810
|
+
style: {
|
|
811
|
+
colors: { primary: "#0B5FFF", accent: "#FF6A00" }
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
async function runInit(cwd = process.cwd()) {
|
|
815
|
+
const target = join4(cwd, CONFIG_FILENAME);
|
|
816
|
+
try {
|
|
817
|
+
await writeFile2(target, JSON.stringify(CONFIG_TEMPLATE, null, 2) + "\n", { flag: "wx" });
|
|
818
|
+
} catch (e) {
|
|
819
|
+
if (e.code === "EEXIST") {
|
|
820
|
+
throw new PptfastError(`${target} already exists \u2014 edit it instead`);
|
|
821
|
+
}
|
|
822
|
+
throw e;
|
|
823
|
+
}
|
|
824
|
+
return `wrote ${target} \u2014 themes: \`pptfast themes\`, style schema: \`pptfast schema --style\``;
|
|
825
|
+
}
|
|
826
|
+
async function runPreview(irPath, outDir, opts = {}) {
|
|
827
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
828
|
+
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
829
|
+
const { raw, baseDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit);
|
|
830
|
+
await applyDeckConfig(raw, { cwd, projectHit, userHit });
|
|
110
831
|
const v = validateIr(raw);
|
|
111
832
|
if (!v.ok) throw new PptfastError(`invalid IR:
|
|
112
833
|
${formatIssues(v.errors)}`);
|
|
113
|
-
await resolveLocalAssets(v.ir,
|
|
114
|
-
await
|
|
834
|
+
await resolveLocalAssets(v.ir, baseDir);
|
|
835
|
+
await mkdir2(outDir, { recursive: true });
|
|
115
836
|
const ir = v.ir;
|
|
837
|
+
const svgs = [];
|
|
116
838
|
for (let i = 0; i < ir.slides.length; i++) {
|
|
839
|
+
const svg = renderSlideSvg(ir, i);
|
|
840
|
+
svgs.push(svg);
|
|
117
841
|
const name = `${String(i + 1).padStart(3, "0")}-${ir.slides[i].type}.svg`;
|
|
118
|
-
await
|
|
842
|
+
await writeFile2(join4(outDir, name), svg);
|
|
843
|
+
}
|
|
844
|
+
const ok = `wrote ${ir.slides.length} SVG files to ${outDir}`;
|
|
845
|
+
const notes = [];
|
|
846
|
+
const aliasNote = normalizedNote(v.normalized);
|
|
847
|
+
if (aliasNote) notes.push(aliasNote);
|
|
848
|
+
if (opts.htmlOut) {
|
|
849
|
+
const htmlPath = join4(outDir, "preview.html");
|
|
850
|
+
const hasPlaceholder = ir.slides.some((slide) => slide.placeholder);
|
|
851
|
+
const auditReport = hasPlaceholder ? void 0 : auditDeck(ir);
|
|
852
|
+
const auditFindings = auditReport?.findings ?? [];
|
|
853
|
+
const html = buildPreviewHtml({
|
|
854
|
+
title: ir.filename,
|
|
855
|
+
slides: ir.slides.map((slide, i) => ({
|
|
856
|
+
index: i,
|
|
857
|
+
id: slide.id,
|
|
858
|
+
type: slide.type,
|
|
859
|
+
svg: svgs[i],
|
|
860
|
+
placeholder: slide.placeholder
|
|
861
|
+
})),
|
|
862
|
+
findings: auditFindings.map((f) => ({ page: f.page, slideId: f.slideId, code: f.code, message: f.message })),
|
|
863
|
+
auditNote: hasPlaceholder ? "audit overlay skipped \u2014 deck has unfilled placeholder pages; fill every page and re-run `pptfast preview --html` to see audit findings" : void 0,
|
|
864
|
+
checks: auditReport?.checks
|
|
865
|
+
});
|
|
866
|
+
await writeFile2(htmlPath, html);
|
|
867
|
+
notes.push(`note: wrote self-contained preview to ${htmlPath}`);
|
|
868
|
+
if (auditFindings.length > 0) {
|
|
869
|
+
notes.push(`note: audit found ${auditFindings.length} finding${auditFindings.length === 1 ? "" : "s"} \u2014 see preview.html`);
|
|
870
|
+
}
|
|
119
871
|
}
|
|
120
|
-
return
|
|
872
|
+
return notes.length > 0 ? `${ok}
|
|
873
|
+
${notes.join("\n")}` : ok;
|
|
874
|
+
}
|
|
875
|
+
function withRewrittenAssetPaths(ir, deckDir, outDir) {
|
|
876
|
+
const images = Object.fromEntries(
|
|
877
|
+
Object.entries(ir.assets.images).map(([id, asset]) => {
|
|
878
|
+
if (asset.src.startsWith("data:") || /^https?:\/\//.test(asset.src)) return [id, asset];
|
|
879
|
+
return [id, { ...asset, src: relative2(outDir, join4(deckDir, asset.src)) }];
|
|
880
|
+
})
|
|
881
|
+
);
|
|
882
|
+
return { ...ir, assets: { images } };
|
|
883
|
+
}
|
|
884
|
+
async function runAssemble(target, opts = {}) {
|
|
885
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
886
|
+
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
887
|
+
const dir = await resolveDeckTarget(target, resolveDecksDirSource(projectHit, userHit), cwd);
|
|
888
|
+
if (await pathExists(dir) && !await isDeckDirectory(dir)) {
|
|
889
|
+
throw new PptfastError(`expected a deck project directory: ${dir}`);
|
|
890
|
+
}
|
|
891
|
+
const { ir, generatedSeed, materializedLayoutCount, deckDir } = await readDeckDir(dir);
|
|
892
|
+
const outPath = opts.output ? resolve5(cwd, opts.output) : join4(deckDir, "deck.json");
|
|
893
|
+
const outDir = dirname2(outPath);
|
|
894
|
+
const outIr = outDir === deckDir ? ir : withRewrittenAssetPaths(ir, deckDir, outDir);
|
|
895
|
+
await mkdir2(outDir, { recursive: true });
|
|
896
|
+
await writeFile2(outPath, JSON.stringify(outIr, null, 2) + "\n");
|
|
897
|
+
const placeholderCount = outIr.slides.filter((s) => s.placeholder).length;
|
|
898
|
+
const summary = `wrote ${outPath} (${outIr.slides.length} slides, ${placeholderCount} placeholder${placeholderCount === 1 ? "" : "s"})`;
|
|
899
|
+
const notes = [];
|
|
900
|
+
if (generatedSeed !== void 0) {
|
|
901
|
+
notes.push(`note: generated seed ${generatedSeed} \u2014 add "seed": ${generatedSeed} to deck.spec.json for revision stability`);
|
|
902
|
+
}
|
|
903
|
+
if (materializedLayoutCount !== void 0) {
|
|
904
|
+
notes.push(
|
|
905
|
+
`note: ${materializedLayoutCount} layout${materializedLayoutCount === 1 ? "" : "s"} auto-selected into deck.json \u2014 pin "layout" in a page file to lock one`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
return [summary, ...notes].join("\n");
|
|
909
|
+
}
|
|
910
|
+
async function runDisassemble(irPath, outDir) {
|
|
911
|
+
const raw = await loadIrFile(irPath);
|
|
912
|
+
const v = validateIr(raw);
|
|
913
|
+
if (!v.ok) throw new PptfastError(`invalid IR:
|
|
914
|
+
${formatIssues(v.errors)}`);
|
|
915
|
+
const { spec: spec2, pages } = disassembleDeck(v.ir);
|
|
916
|
+
const ids = Object.keys(pages);
|
|
917
|
+
for (const id of ids) assertSafeFileSegment(id, "slide id");
|
|
918
|
+
const specPath = join4(outDir, "deck.spec.json");
|
|
919
|
+
await mkdir2(outDir, { recursive: true });
|
|
920
|
+
try {
|
|
921
|
+
await writeFile2(specPath, JSON.stringify(spec2, null, 2) + "\n", { flag: "wx" });
|
|
922
|
+
} catch (e) {
|
|
923
|
+
if (e.code === "EEXIST") {
|
|
924
|
+
throw new PptfastError(`${specPath} already exists \u2014 refusing to overwrite an existing deck project`);
|
|
925
|
+
}
|
|
926
|
+
throw e;
|
|
927
|
+
}
|
|
928
|
+
const pagesDir = join4(outDir, "pages");
|
|
929
|
+
try {
|
|
930
|
+
if (ids.length > 0) {
|
|
931
|
+
await mkdir2(pagesDir, { recursive: true });
|
|
932
|
+
await Promise.all(
|
|
933
|
+
ids.map((id) => {
|
|
934
|
+
const content = pages[id];
|
|
935
|
+
return writeFile2(join4(pagesDir, `${id}.json`), JSON.stringify(content, null, 2) + "\n");
|
|
936
|
+
})
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
const { count: assetCount, assetsDir } = await writeDeckAssets(
|
|
940
|
+
v.ir.assets.images,
|
|
941
|
+
outDir,
|
|
942
|
+
dirname2(resolve5(irPath))
|
|
943
|
+
);
|
|
944
|
+
const pagesNote = ids.length > 0 ? `${ids.length} page file${ids.length === 1 ? "" : "s"} to ${pagesDir}` : "no pages (every slide was a placeholder)";
|
|
945
|
+
const assetsNote = assetCount > 0 ? `, and ${assetCount} asset file${assetCount === 1 ? "" : "s"} to ${assetsDir}` : "";
|
|
946
|
+
return `wrote ${specPath}, ${pagesNote}${assetsNote}`;
|
|
947
|
+
} catch (e) {
|
|
948
|
+
await rm(specPath, { force: true }).catch(() => {
|
|
949
|
+
});
|
|
950
|
+
throw e;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async function runMigrate(input, output, cwd = process.cwd()) {
|
|
954
|
+
const resolvedInput = resolve5(cwd, input);
|
|
955
|
+
if (await isDeckDirectory(resolvedInput)) {
|
|
956
|
+
return runMigrateDeckDir(resolvedInput, output, cwd);
|
|
957
|
+
}
|
|
958
|
+
return runMigrateIrFile(resolvedInput, output, cwd);
|
|
959
|
+
}
|
|
960
|
+
async function runMigrateDeckDir(dir, output, cwd) {
|
|
961
|
+
const planPath = join4(dir, PLAN_FILENAME);
|
|
962
|
+
const sourceSpecPath = join4(dir, SPEC_FILENAME);
|
|
963
|
+
if (!await pathExists(planPath) && await pathExists(sourceSpecPath)) {
|
|
964
|
+
throw new PptfastError(
|
|
965
|
+
`${dir} has ${SPEC_FILENAME} but no ${PLAN_FILENAME} \u2014 this deck project is already migrated, nothing to do`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const raw = await loadIrFile(planPath, "plan");
|
|
969
|
+
const migrated = migrateDeckPlanToSpec(raw);
|
|
970
|
+
const outDir = resolve5(cwd, output);
|
|
971
|
+
const specPath = join4(outDir, SPEC_FILENAME);
|
|
972
|
+
await mkdir2(outDir, { recursive: true });
|
|
973
|
+
try {
|
|
974
|
+
await writeFile2(specPath, JSON.stringify(migrated, null, 2) + "\n", { flag: "wx" });
|
|
975
|
+
} catch (e) {
|
|
976
|
+
if (e.code === "EEXIST") {
|
|
977
|
+
throw new PptfastError(`${specPath} already exists \u2014 refusing to overwrite, delete it first or choose a different -o`);
|
|
978
|
+
}
|
|
979
|
+
throw e;
|
|
980
|
+
}
|
|
981
|
+
return `wrote ${specPath} \u2014 run \`pptfast spec validate ${specPath}\` to confirm it, then delete ${planPath} (a directory with both files present is rejected)`;
|
|
982
|
+
}
|
|
983
|
+
async function runMigrateIrFile(filePath, output, cwd) {
|
|
984
|
+
const raw = await loadIrFile(filePath);
|
|
985
|
+
const version = typeof raw === "object" && raw !== null ? raw.version : void 0;
|
|
986
|
+
if (version === "2") {
|
|
987
|
+
throw new PptfastError(
|
|
988
|
+
"pptfast migrate does not support IR v2 (spec \xA715.3: v2 has no real users) \u2014 run `pptfast validate` on the v2 file to see the full v2\u2192v4 combined field mapping and rewrite it by hand"
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (version !== "3") {
|
|
992
|
+
throw new PptfastError(
|
|
993
|
+
`pptfast migrate only converts an IR v3 file (version: "3") or a deck project directory containing ${PLAN_FILENAME} \u2014 got version ${JSON.stringify(version)} in ${filePath}`
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
const parsed = PptxIRV3Schema.safeParse(raw);
|
|
997
|
+
if (!parsed.success) {
|
|
998
|
+
const detail = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
|
|
999
|
+
throw new PptfastError(`invalid IR v3 file ${filePath}:
|
|
1000
|
+
${detail}`);
|
|
1001
|
+
}
|
|
1002
|
+
const migrated = migrateIrV3ToV4(parsed.data);
|
|
1003
|
+
const outPath = resolve5(cwd, output);
|
|
1004
|
+
await mkdir2(dirname2(outPath), { recursive: true });
|
|
1005
|
+
try {
|
|
1006
|
+
await writeFile2(outPath, JSON.stringify(migrated, null, 2) + "\n", { flag: "wx" });
|
|
1007
|
+
} catch (e) {
|
|
1008
|
+
if (e.code === "EEXIST") {
|
|
1009
|
+
throw new PptfastError(`${outPath} already exists \u2014 refusing to overwrite, delete it first or choose a different -o`);
|
|
1010
|
+
}
|
|
1011
|
+
throw e;
|
|
1012
|
+
}
|
|
1013
|
+
return `wrote ${outPath} (migrated IR v3 \u2192 v4)`;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/cli/update.ts
|
|
1017
|
+
import { execFile } from "child_process";
|
|
1018
|
+
var PACKAGE_NAME = "@liustack/pptfast";
|
|
1019
|
+
function normalizeVersion(version) {
|
|
1020
|
+
const normalized = version.trim().replace(/^v/i, "").split("-")[0];
|
|
1021
|
+
if (!normalized) throw new Error(`invalid version: ${version}`);
|
|
1022
|
+
return normalized;
|
|
1023
|
+
}
|
|
1024
|
+
function parseVersion(version) {
|
|
1025
|
+
return normalizeVersion(version).split(".").map((segment) => {
|
|
1026
|
+
const value = Number.parseInt(segment, 10);
|
|
1027
|
+
if (!Number.isFinite(value)) throw new Error(`invalid version segment: ${segment}`);
|
|
1028
|
+
return value;
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
function compareVersions(left, right) {
|
|
1032
|
+
const l = parseVersion(left);
|
|
1033
|
+
const r = parseVersion(right);
|
|
1034
|
+
for (let i = 0; i < Math.max(l.length, r.length); i++) {
|
|
1035
|
+
const a = l[i] ?? 0;
|
|
1036
|
+
const b = r[i] ?? 0;
|
|
1037
|
+
if (a !== b) return a < b ? -1 : 1;
|
|
1038
|
+
}
|
|
1039
|
+
return 0;
|
|
1040
|
+
}
|
|
1041
|
+
var runCommand = (command, args) => new Promise((resolve6, reject) => {
|
|
1042
|
+
execFile(command, args, { encoding: "utf8" }, (error, stdout, stderr) => {
|
|
1043
|
+
if (error) {
|
|
1044
|
+
reject(new Error(stderr.trim() || error.message));
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
resolve6(stdout.trim());
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
async function checkForUpdate({
|
|
1051
|
+
currentVersion,
|
|
1052
|
+
packageName = PACKAGE_NAME,
|
|
1053
|
+
run = runCommand
|
|
1054
|
+
}) {
|
|
1055
|
+
const current = normalizeVersion(currentVersion);
|
|
1056
|
+
try {
|
|
1057
|
+
const latest = normalizeVersion(await run("npm", ["view", packageName, "version"]));
|
|
1058
|
+
return {
|
|
1059
|
+
packageName,
|
|
1060
|
+
currentVersion: current,
|
|
1061
|
+
latestVersion: latest,
|
|
1062
|
+
updateAvailable: compareVersions(current, latest) < 0,
|
|
1063
|
+
checked: true
|
|
1064
|
+
};
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
return {
|
|
1067
|
+
packageName,
|
|
1068
|
+
currentVersion: current,
|
|
1069
|
+
latestVersion: null,
|
|
1070
|
+
updateAvailable: false,
|
|
1071
|
+
checked: false,
|
|
1072
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
function createSelfUpdater(run = runCommand) {
|
|
1077
|
+
return async ({
|
|
1078
|
+
currentVersion,
|
|
1079
|
+
packageName = PACKAGE_NAME
|
|
1080
|
+
}) => {
|
|
1081
|
+
const info = await checkForUpdate({ currentVersion, packageName, run });
|
|
1082
|
+
if (!info.checked) throw new Error(`unable to check for updates: ${info.error ?? "unknown error"}`);
|
|
1083
|
+
if (!info.updateAvailable) return { ...info, updated: false };
|
|
1084
|
+
await run("npm", ["install", "-g", `${packageName}@latest`]);
|
|
1085
|
+
return { ...info, updated: true };
|
|
1086
|
+
};
|
|
121
1087
|
}
|
|
122
1088
|
|
|
123
1089
|
// src/cli.ts
|
|
@@ -128,25 +1094,109 @@ function fail(e) {
|
|
|
128
1094
|
console.error(e instanceof Error ? e.message : String(e));
|
|
129
1095
|
process.exit(1);
|
|
130
1096
|
}
|
|
131
|
-
program.command("render").description("Render an IR JSON file to a .pptx").argument("<
|
|
1097
|
+
program.command("render").description("Render an IR JSON file, deck project directory, or bare deck name to a .pptx").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptfast/decks").requiredOption("-o, --output <file>", "output .pptx path").option("--theme <id>", "override the deck theme (see `pptfast themes`)").option("--style <path>", "style overrides JSON re-coloring the theme (see `pptfast schema --style`)").option("--draft", "allow unfilled placeholder pages (skip the draft gate)").action(async (target, opts) => {
|
|
1098
|
+
try {
|
|
1099
|
+
console.log(
|
|
1100
|
+
await runRender(target, {
|
|
1101
|
+
output: opts.output,
|
|
1102
|
+
theme: opts.theme,
|
|
1103
|
+
stylePath: opts.style,
|
|
1104
|
+
draft: opts.draft
|
|
1105
|
+
})
|
|
1106
|
+
);
|
|
1107
|
+
} catch (e) {
|
|
1108
|
+
fail(e);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
program.command("validate").description("Validate an IR JSON file, deck project directory, or bare deck name against the schema").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptfast/decks").action(async (target) => {
|
|
1112
|
+
try {
|
|
1113
|
+
console.log(await runValidate(target));
|
|
1114
|
+
} catch (e) {
|
|
1115
|
+
fail(e);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
program.command("audit").description(
|
|
1119
|
+
"Deterministic geometry audit (overflow, out-of-bounds, low-contrast, overlap, content-truncated, content-dropped), plus an optional --pixels contrast pass \u2014 exits 1 when it finds anything"
|
|
1120
|
+
).argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptfast/decks").option("--json", "machine-readable output (the full AuditReport)").option("--pixels", "also run the optional pixel-contrast pass over image-backed text (requires sharp)").action(async (target, opts) => {
|
|
1121
|
+
try {
|
|
1122
|
+
const { output, hasFindings } = await runAudit(target, { json: opts.json, pixels: opts.pixels });
|
|
1123
|
+
console.log(output);
|
|
1124
|
+
if (hasFindings) process.exit(1);
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
fail(e);
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
program.command("schema").description("Print the IR JSON Schema (feed this to a model before it writes IR)").option("--style", "print the style-override schema instead").option("--spec", "print the deck spec schema instead").option("--plan", "removed \u2014 use --spec instead").action((opts) => {
|
|
1130
|
+
if (opts.plan) {
|
|
1131
|
+
fail(new Error("`pptfast schema --plan` has been renamed to `pptfast schema --spec` \u2014 run `pptfast schema --spec` instead"));
|
|
1132
|
+
}
|
|
1133
|
+
console.log(runSchema(opts.spec ? "spec" : opts.style ? "style" : void 0));
|
|
1134
|
+
});
|
|
1135
|
+
var plan = program.command("plan").description("Removed \u2014 use `pptfast spec` instead");
|
|
1136
|
+
plan.command("validate").description("Removed \u2014 use `pptfast spec validate` instead").argument("<file>").action(() => {
|
|
1137
|
+
fail(new Error("`pptfast plan validate` has been renamed to `pptfast spec validate` \u2014 run `pptfast spec validate <file>` instead"));
|
|
1138
|
+
});
|
|
1139
|
+
var spec = program.command("spec").description("Deck spec commands (spec \xA76)");
|
|
1140
|
+
spec.command("validate").description("Validate a deck spec JSON file against the schema and strategy-aware hard gates").argument("<spec.json>").action(async (specPath) => {
|
|
1141
|
+
try {
|
|
1142
|
+
console.log(await runSpecValidate(specPath));
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
fail(e);
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
program.command("assemble").description("Assemble a deck project directory (deck.spec.json + pages/ + assets/) into an IR JSON file").argument("<dir|name>", "deck project directory, or bare name under ~/.pptfast/decks").option("-o, --output <file>", "output IR JSON path (default: <dir>/deck.json)").action(async (target, opts) => {
|
|
1148
|
+
try {
|
|
1149
|
+
console.log(await runAssemble(target, { output: opts.output }));
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
fail(e);
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
program.command("disassemble").description("Split an IR JSON file into a deck project directory (deck.spec.json + pages/)").argument("<ir.json>", "path to the IR file").requiredOption("-o, --output <dir>", "output deck project directory").action(async (irPath, opts) => {
|
|
132
1155
|
try {
|
|
133
|
-
console.log(await
|
|
1156
|
+
console.log(await runDisassemble(irPath, opts.output));
|
|
134
1157
|
} catch (e) {
|
|
135
1158
|
fail(e);
|
|
136
1159
|
}
|
|
137
1160
|
});
|
|
138
|
-
program.command("
|
|
1161
|
+
program.command("migrate").description("Convert a v3 IR file to v4, or a deck.plan.json project directory to deck.spec.json \u2014 deterministic, no model").argument("<input>", "IR v3 JSON file, or a deck project directory containing deck.plan.json").requiredOption("-o, --output <output>", "output path \u2014 an IR JSON file for a v3 file input, a directory for a deck-project-directory input").action(async (input, opts) => {
|
|
139
1162
|
try {
|
|
140
|
-
console.log(await
|
|
1163
|
+
console.log(await runMigrate(input, opts.output));
|
|
141
1164
|
} catch (e) {
|
|
142
1165
|
fail(e);
|
|
143
1166
|
}
|
|
144
1167
|
});
|
|
145
|
-
program.command("schema").description("Print the IR JSON Schema (feed this to a model before it writes IR)").action(() => console.log(runSchema()));
|
|
146
1168
|
program.command("themes").description("List built-in themes").option("--json", "machine-readable output").action((opts) => console.log(runThemes(Boolean(opts.json))));
|
|
147
|
-
program.command("
|
|
1169
|
+
program.command("narratives").description("List named narrative presets (strategy/pacing/audience axes + theme recommendations)").option("--json", "machine-readable output").action((opts) => console.log(runNarratives(Boolean(opts.json))));
|
|
1170
|
+
program.command("scenarios").description("Removed \u2014 use `pptfast narratives` instead").action(() => {
|
|
1171
|
+
fail(new Error("`pptfast scenarios` has been renamed to `pptfast narratives` \u2014 run `pptfast narratives` instead"));
|
|
1172
|
+
});
|
|
1173
|
+
program.command("init").description("Scaffold a pptfast.config.json in the current directory").action(async () => {
|
|
1174
|
+
try {
|
|
1175
|
+
console.log(await runInit());
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
fail(e);
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
program.command("preview").description("Render each slide to an SVG file for visual self-check").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptfast/decks").requiredOption("-o, --output <dir>", "output directory").option("--html", "also write a self-contained preview.html (all slides inlined \u2014 thumbnail strip, keyboard navigation) for human review").action(async (target, opts) => {
|
|
1181
|
+
try {
|
|
1182
|
+
console.log(await runPreview(target, opts.output, { htmlOut: opts.html }));
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
fail(e);
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
program.command("check-update").description("Check npm for a newer pptfast release").action(async () => {
|
|
1188
|
+
const info = await checkForUpdate({ currentVersion: VERSION });
|
|
1189
|
+
if (!info.checked) fail(new Error(`update check failed: ${info.error}`));
|
|
1190
|
+
console.log(
|
|
1191
|
+
info.updateAvailable ? `update available: ${info.currentVersion} \u2192 ${info.latestVersion} (run \`pptfast self-update\`)` : `pptfast ${info.currentVersion} is up to date`
|
|
1192
|
+
);
|
|
1193
|
+
});
|
|
1194
|
+
program.command("self-update").description("Update the global pptfast install to the latest release").action(async () => {
|
|
148
1195
|
try {
|
|
149
|
-
|
|
1196
|
+
const result = await createSelfUpdater()({ currentVersion: VERSION });
|
|
1197
|
+
console.log(
|
|
1198
|
+
result.updated ? `updated: ${result.currentVersion} \u2192 ${result.latestVersion}` : `already at the latest version (${result.currentVersion})`
|
|
1199
|
+
);
|
|
150
1200
|
} catch (e) {
|
|
151
1201
|
fail(e);
|
|
152
1202
|
}
|